Editor: unified macOS-style Settings window (sidebar, icons, grouped sections)

One Settings window replaces the four scattered modals: searchable sidebar with
colored icon chips grouped as Editor / Generation (Engine, Models & Pipeline) /
Learning (Refinement & Taste) / System (Updates & ComfyUI, Temp Files). Sections
self-register on a small shell (settings_window.js); the legacy modal facades
(EditorSettingsModal/EngineSettingsModal/ModelsModal/TempBrowserModal) deep-link
into their section so every existing entry point still works. Refinement-key and
restart/update actions get proper panels reusing the single menubar
implementations (window.FunPackMaintenance). Cmd+, opens it; tour's overlay
sweep now closes the window through its cleanup hooks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-07-05 15:16:09 +03:00
co-authored by Claude Fable 5
parent 9195906c03
commit 86f58ca074
9 changed files with 433 additions and 114 deletions
+19 -22
View File
@@ -1,14 +1,11 @@
// Editor Settings modal: per-browser editor preferences (autocomplete, anchor).
// Opened from the Settings menu. Distinct from Engine settings (which configures
// Studio / Chain Sampler behavior on the project).
// Editor settings: per-browser editor preferences (autocomplete, anchor).
// A section of the unified Settings window. Distinct from Engine settings
// (which configures Studio / Chain Sampler behavior on the project).
(function () {
const { el } = window.dom;
const S = window.Store;
const API = window.MovieEditorAPI;
let overlay = null;
function close() { if (overlay) { overlay.remove(); overlay = null; } }
function labeled(text, control) {
const l = el("label", "field"); l.append(el("span", null, text)); l.append(control); return l;
}
@@ -155,17 +152,8 @@
return row;
}
function open() {
close();
overlay = el("div", "modal-overlay");
const box = el("div", "modal");
const head = el("div", "modal-head");
head.append(el("div", "modal-title", "Editor settings"));
const hr = el("div", "modal-head-right");
const x = el("button", "btn ghost tiny", "✕"); x.onclick = close;
hr.append(x); head.append(hr); box.append(head);
const content = el("div", "modal-content es-content");
function mount(body) {
const content = el("div", "es-content");
content.append(toggleRow(
"Prompt autocomplete",
"Suggest matching shortcuts while you type in the global prompt and scene prompts. Shows the trigger, its prompt, and category.",
@@ -175,11 +163,20 @@
"Text before the first split trigger is a shared anchor prepended to every scene. Turn off to make that leading text Scene 1 instead.",
"anchorEnabled"));
content.append(anchorGuideSection());
box.append(content); overlay.append(box);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
document.body.append(overlay);
body.append(content);
}
window.EditorSettingsModal = { open, close };
window.SettingsWindow.register({
id: "editor", group: "", title: "Editor",
subtitle: "Per-browser preferences — they apply to this browser, not the project.",
keywords: "autocomplete anchor prompt shortcuts i2v bypass guide preferences",
iconBg: "linear-gradient(180deg,#6aa9ff,#3b6fd9)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round"><path d="M2 4.5h12M2 8h12M2 11.5h12"/><circle cx="6" cy="4.5" r="1.7" fill="#fff" stroke="none"/><circle cx="11" cy="8" r="1.7" fill="#fff" stroke="none"/><circle cx="5" cy="11.5" r="1.7" fill="#fff" stroke="none"/></svg>',
mount,
});
window.EditorSettingsModal = {
open: () => window.SettingsWindow.open("editor"),
close: () => window.SettingsWindow.close(),
};
})();
+30 -37
View File
@@ -1,12 +1,12 @@
// Engine settings modal: Studio, Chain Sampler, continuity (moved out of Project inspector).
// Engine settings: Studio, Chain Sampler, continuity (moved out of Project inspector).
// A section of the unified Settings window.
(function () {
const { el, clear } = window.dom;
const S = window.Store;
let overlay = null;
let _mounted = null; // { scroller, content }
let unsub = null;
let _editing = false;
let _onKey = null;
const FOLD_KEY = "funpack_engine_fold_";
function foldOpen(id, def) {
@@ -599,44 +599,22 @@
}
function render() {
if (!overlay) return;
const content = overlay.querySelector(".modal-content");
if (!_mounted) return;
const { scroller, content } = _mounted;
if (_editing) {
if (shouldProtect(document.activeElement, content)) return;
_editing = false;
}
const scrollTop = content.scrollTop;
const scrollTop = scroller.scrollTop;
clear(content);
renderContent(content, S.get());
content.scrollTop = scrollTop;
scroller.scrollTop = scrollTop;
}
function close() {
if (unsub) { unsub(); unsub = null; }
if (_onKey) { document.removeEventListener("keydown", _onKey); _onKey = null; }
if (overlay) overlay.remove();
overlay = null;
_editing = false;
}
function open() {
if (!S.get().project) return;
if (overlay) { render(); return; }
overlay = el("div", "modal-overlay");
const modal = el("div", "modal modal-wide");
const head = el("div", "modal-head");
head.append(el("div", "modal-title", "Engine Settings"));
const closeBtn = el("button", "btn ghost", "✕");
closeBtn.onclick = close;
const heRight = el("div", "modal-head-right"); heRight.append(closeBtn);
head.append(heRight);
modal.append(head);
const content = el("div", "modal-content engine-modal-content");
modal.append(content);
overlay.append(modal);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
document.body.append(overlay);
function mount(body) {
const content = el("div", "engine-modal-content");
body.append(content);
_mounted = { scroller: body, content };
content.addEventListener("focusin", (e) => {
const t = e.target;
@@ -649,12 +627,27 @@
setTimeout(() => { if (!_editing) render(); }, 60);
});
_onKey = (e) => { if (e.key === "Escape") close(); };
document.addEventListener("keydown", _onKey);
unsub = S.subscribe(() => render());
render();
return () => {
if (unsub) { unsub(); unsub = null; }
_mounted = null;
_editing = false;
};
}
window.EngineSettingsModal = { open, close };
window.SettingsWindow.register({
id: "engine", group: "Generation", order: 1, title: "Engine",
subtitle: "FunPack Studio, Chain Sampler, and continuity for the open project.",
keywords: "studio chain sampler refinement key seed cfg guidance continuity guides presets "
+ "embed dynashift joyai decode transition overlap steer adjustments temporal",
iconBg: "linear-gradient(180deg,#ffb64d,#e07f1f)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13"><path d="M9.2 1.3 3 9h4.1l-1 5.7L12.9 7H8.5l.7-5.7z" fill="#fff"/></svg>',
mount,
});
window.EngineSettingsModal = {
open: () => window.SettingsWindow.open("engine"),
close: () => window.SettingsWindow.close(),
};
})();
+1
View File
@@ -103,6 +103,7 @@
<script src="api.js"></script>
<script src="dom.js"></script>
<script src="settings_window.js"></script>
<script src="floating_window.js"></script>
<script src="restart_ui.js"></script>
<script src="git_actions.js"></script>
+12
View File
@@ -219,6 +219,8 @@
} },
],
Settings: [
{ label: "All Settings…", hint: "⌘,", action: () => window.SettingsWindow.open() },
{ sep: true },
{ label: "Editor settings…", action: () => window.EditorSettingsModal.open() },
// Never grey this out over pipeline state: the modal itself explains WHY Studio /
// Chain Sampler knobs are unavailable (built-in pipeline disabled, custom slots)
@@ -363,4 +365,14 @@
});
S.subscribe((st) => { render(); renderChips(st); });
render();
// Single implementations for maintenance actions, shared with the Settings window
// (Refinement & Taste + Updates & ComfyUI sections) — menus stay as shortcuts.
window.FunPackMaintenance = {
exportRefinementKey,
importRefinementKeyFile: () => refinementKeyImportInput.click(),
deleteRefinementKey,
clearGlobalTaste,
restartComfy,
};
})();
+43 -25
View File
@@ -1,5 +1,6 @@
// "Models" settings modal: configure pluggable node slots for the fixed pipeline.
// "Models" settings section: configure pluggable node slots for the fixed pipeline.
// Pick a Model Type -> a Loader Node -> the editor exposes that node's inputs.
// Lives in the unified Settings window; ModelsModal.open() deep-links to it.
(function () {
const { el, clear } = window.dom;
const API = window.MovieEditorAPI;
@@ -15,7 +16,7 @@
let coreProducers = []; // [{id,type,label}]
let requirements = []; // [{id,type,label,required,role_hint,hint}]
let wiringRules = {}; // guided wiring rules from /pipeline-ports
let overlay = null;
let container = null; // mounted content root inside the Settings window
const expanded = new Set(); // slot ids currently expanded (collapsed by default)
let linkMode = false; // selecting inputs to bind into one shared control
let linkSel = []; // [{slotId, input, kind, choices, label}]
@@ -1095,10 +1096,9 @@
}
function render() {
if (!overlay) return;
const content = overlay.querySelector(".modal-content");
clear(content);
content.append(body());
if (!container) return;
clear(container);
container.append(body());
}
async function prewarmSpecs() {
@@ -1111,8 +1111,7 @@
render();
}
async function open() {
if (overlay) return; // already open — don't stack a second modal (orphans handlers)
async function loadAll() {
await ensureRoles();
try {
const pp = await API.pipelinePorts();
@@ -1129,23 +1128,42 @@
// metadata was captured, so opening Models once upgrades existing projects. persist()
// dispatches funpack-models-changed → the store reloads and the inspector re-renders.
if (refreshExposedChoices()) { try { await persist(); } catch (_) {} }
overlay = el("div", "modal-overlay");
const modal = el("div", "modal");
const head = el("div", "modal-head");
head.append(el("div", "modal-title", "Models & Pipeline Nodes"));
const refresh = el("button", "btn ghost", "↻ Refresh model list"); refresh.onclick = refreshList;
const closeModal = () => { if (overlay) overlay.remove(); overlay = null; };
const close = el("button", "btn ghost", "✕"); close.onclick = closeModal;
const heRight = el("div", "modal-head-right"); heRight.append(refresh); heRight.append(close);
head.append(heRight);
modal.append(head);
modal.append(el("div", "modal-content"));
overlay.append(modal);
overlay.addEventListener("click", (e) => { if (overlay && e.target === overlay) closeModal(); });
document.body.append(overlay);
render();
}
window.ModelsModal = { open, refresh: async () => { await ensureRoles().catch(() => {}); await doFullRefresh().catch(() => {}); } };
function mount(body, ctx) {
container = el("div", "models-mount");
container.append(el("div", "pj-meta", "Loading models & pipeline…"));
body.append(container);
const refresh = el("button", "btn ghost tiny", "↻ Refresh model list");
refresh.onclick = refreshList;
ctx.setActions([refresh]);
loadAll()
.then(() => { if (container && container.isConnected) render(); })
.catch(() => {
if (container && container.isConnected) {
clear(container);
container.append(el("div", "pj-meta", "Could not load models & pipeline."));
}
});
return () => { container = null; };
}
window.SettingsWindow.register({
id: "models", group: "Generation", order: 2, title: "Models & Pipeline",
subtitle: "Loaders and custom nodes wired into the fixed FunPack pipeline.",
keywords: "models loaders unet vae clip lora nodes pipeline wiring workflow import "
+ "bypass disable core full control links",
iconBg: "linear-gradient(180deg,#b18cff,#7a4fd0)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"><path d="M8 1.8 14 5v6l-6 3.2L2 11V5l6-3.2z"/><path d="M2 5l6 3 6-3M8 8v6.2"/></svg>',
mount,
});
window.ModelsModal = {
open: () => window.SettingsWindow.open("models"),
refresh: async () => {
await ensureRoles().catch(() => {});
await doFullRefresh().catch(() => {});
if (container) render();
},
};
})();
+247
View File
@@ -0,0 +1,247 @@
// Unified Settings window, macOS System Settings style: a sidebar of grouped,
// searchable sections (colored icon chips) + one content pane. Sections self-register
// via SettingsWindow.register(); the legacy per-modal facades (EditorSettingsModal,
// EngineSettingsModal, ModelsModal, TempBrowserModal) deep-link into their section,
// so every existing entry point (menus, inspector, workflow import) keeps working.
(function () {
const { el, clear } = window.dom;
const GROUPS = ["", "Generation", "Learning", "System"];
const registry = [];
let overlay = null, activeId = null, cleanup = null, query = "";
let navEl, bodyEl, titleEl, subEl, actionsEl;
function register(spec) {
registry.push(spec);
if (overlay) renderNav();
}
// Sidebar order: group order, then explicit spec.order, then registration order.
function orderedSections() {
const out = [];
GROUPS.forEach((g) => {
out.push(...registry
.filter((s) => (s.group || "") === g)
.sort((a, b) => (a.order ?? 99) - (b.order ?? 99)));
});
return out;
}
function matches(spec) {
if (!query) return true;
return (spec.title + " " + (spec.subtitle || "") + " " + (spec.keywords || ""))
.toLowerCase().includes(query);
}
function iconEl(spec) {
const w = el("span", "sw-icon");
if (spec.iconBg) w.style.background = spec.iconBg;
w.innerHTML = spec.icon || "";
return w;
}
function renderNav() {
clear(navEl);
const ordered = orderedSections();
GROUPS.forEach((g) => {
const items = ordered.filter((s) => (s.group || "") === g && matches(s));
if (!items.length) return;
if (g) navEl.append(el("div", "sw-group-title", g));
items.forEach((s) => {
const it = el("div", "sw-item" + (s.id === activeId ? " active" : ""));
it.append(iconEl(s), el("span", "sw-item-label", s.title));
it.onclick = () => show(s.id);
navEl.append(it);
});
});
if (!navEl.children.length) navEl.append(el("div", "sw-nav-empty", "No matches"));
}
function teardownSection() {
if (typeof cleanup === "function") { try { cleanup(); } catch (_) {} }
cleanup = null;
}
function show(id) {
const spec = registry.find((s) => s.id === id) || orderedSections()[0];
if (!spec || !overlay) return;
teardownSection();
activeId = spec.id;
titleEl.textContent = spec.title;
subEl.textContent = spec.subtitle || "";
subEl.hidden = !spec.subtitle;
clear(actionsEl);
clear(bodyEl);
bodyEl.scrollTop = 0;
const ctx = {
setActions: (nodes) => { clear(actionsEl); (nodes || []).forEach((n) => actionsEl.append(n)); },
openSection: (sid) => show(sid),
};
cleanup = spec.mount(bodyEl, ctx) || null;
renderNav();
}
function close() {
teardownSection();
if (overlay) overlay.remove();
overlay = null; activeId = null;
}
function open(id) {
// Recover if something removed our overlay from the DOM directly
// (e.g. the tour's closeModalOverlay sweep) without calling close().
if (overlay && !overlay.isConnected) { close(); }
if (overlay) { show(id || activeId); return; }
query = "";
overlay = el("div", "modal-overlay sw-overlay");
const win = el("div", "settings-win");
const side = el("div", "sw-side");
const search = el("input", "sw-search");
search.type = "search"; search.placeholder = "Search"; search.autocomplete = "off";
search.oninput = () => { query = search.value.trim().toLowerCase(); renderNav(); };
navEl = el("div", "sw-nav");
side.append(search, navEl);
const main = el("div", "sw-main");
const head = el("div", "sw-head");
const ht = el("div", "sw-head-text");
titleEl = el("div", "sw-head-title");
subEl = el("div", "sw-head-sub");
ht.append(titleEl, subEl);
actionsEl = el("div", "sw-head-actions");
const x = el("button", "btn ghost tiny sw-close", "✕");
x.onclick = close;
head.append(ht, actionsEl, x);
bodyEl = el("div", "sw-body");
main.append(head, bodyEl);
win.append(side, main);
overlay.append(win);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
document.body.append(overlay);
show(id);
}
window.addEventListener("keydown", (e) => {
if (overlay && e.key === "Escape") { close(); return; }
if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key === ",") {
e.preventDefault();
overlay ? close() : open();
}
});
window.SettingsWindow = { open, close, register };
// ── shared row builders for settings panels ────────────────────────────
function actionRow(title, hint, btnLabel, onClick, opts = {}) {
const row = el("div", "sw-row");
const main = el("div", "sw-row-main");
main.append(el("div", "sw-row-title", title));
if (hint) main.append(el("div", "sw-row-hint", hint));
row.append(main);
const btn = el("button", "btn ghost tiny" + (opts.danger ? " danger" : ""), btnLabel);
btn.type = "button";
if (opts.disabled) btn.disabled = true;
btn.onclick = onClick;
row.append(btn);
return row;
}
function infoRow(title, value, ok) {
const row = el("div", "sw-row");
const main = el("div", "sw-row-main");
main.append(el("div", "sw-row-title", title));
row.append(main);
const val = el("span", "sw-row-value", value);
if (ok != null) {
const led = el("span", "sw-led " + (ok ? "ok" : "bad"));
row.append(led);
}
row.append(val);
return row;
}
// ── built-in section: Refinement & Taste (Learning) ────────────────────
// Buttons reuse the single implementations exported by menubar.js
// (window.FunPackMaintenance) — no duplicated logic, just a second door.
register({
id: "refinement", group: "Learning", title: "Refinement & Taste",
subtitle: "Learned-taste state: refinement keys and the Absolute global-taste store.",
keywords: "refinement key export import delete clear absolute taste store learning ratings",
iconBg: "linear-gradient(180deg,#ff8bc2,#d84f92)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13"><path d="M8 1.5 9.6 6.4 14.5 8 9.6 9.6 8 14.5 6.4 9.6 1.5 8 6.4 6.4 8 1.5z" fill="#fff"/></svg>',
mount(body) {
const M = window.FunPackMaintenance || {};
body.append(el("div", "sw-hint",
"Refinement keys hold what FunPack has learned from your ratings for a named session. "
+ "They are stored on this ComfyUI instance as <key>.json plus sidecar banks."));
const rows = el("div", "sw-rows");
rows.append(actionRow("Export refinement key", "Download a key as <key>.json.", "⬇ Export…", () => M.exportRefinementKey?.()));
rows.append(actionRow("Import refinement key", "Load a previously exported <key>.json onto this instance.", "Import…", () => M.importRefinementKeyFile?.()));
body.append(rows);
body.append(el("div", "sw-rows-label", "Danger zone"));
const danger = el("div", "sw-rows");
danger.append(actionRow("Delete refinement key",
"Removes a key AND all its sidecars (value function, blessed banks, creativity latent, velocity memory).",
"Delete…", () => M.deleteRefinementKey?.(), { danger: true }));
danger.append(actionRow("Clear global taste store",
"Wipes the Absolute store pooled from every rated run (applied in absolute/both steer mode).",
"Clear…", () => M.clearGlobalTaste?.(), { danger: true }));
body.append(danger);
},
});
// ── built-in section: Updates & ComfyUI (System) ───────────────────────
register({
id: "system", group: "System", order: 1, title: "Updates & ComfyUI",
subtitle: "Server connection, FunPack code updates, pipeline health.",
keywords: "update git branch restart comfyui server connection health template version",
iconBg: "linear-gradient(180deg,#a7adb8,#6b7280)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="2.2"/><path d="M8 1.6v2M8 12.4v2M1.6 8h2M12.4 8h2M3.5 3.5l1.4 1.4M11.1 11.1l1.4 1.4M12.5 3.5l-1.4 1.4M4.9 11.1l-1.4 1.4"/></svg>',
mount(body) {
const S = window.Store, G = window.FunPackGit, M = window.FunPackMaintenance || {};
const wrap = el("div", "sw-stack");
body.append(wrap);
function render() {
clear(wrap);
const st = S ? S.get() : {};
const git = G ? G.get() : null;
const gitOk = !!(git && git.ok);
wrap.append(el("div", "sw-rows-label", "ComfyUI server"));
const srv = el("div", "sw-rows");
srv.append(infoRow("Connection", st.health?.ok ? "Connected · " + window.location.host : "Not reachable", !!st.health?.ok));
srv.append(actionRow("Open ComfyUI", "The node-graph UI, in a new tab.", "↗ Open", () => window.open("/", "_blank")));
srv.append(actionRow("Restart ComfyUI", "Down for ~10–40s; any running generation is lost. Reloads automatically.", "⟳ Restart…", () => M.restartComfy?.(), { danger: true }));
wrap.append(srv);
wrap.append(el("div", "sw-rows-label", "FunPack code"));
const code = el("div", "sw-rows");
code.append(infoRow("Branch", gitOk
? git.branch + (git.commit ? " @ " + git.commit : "") + (git.dirty ? " (local changes)" : "")
: (git?.detail || "unavailable")));
code.append(actionRow("Switch branch", "Check out another FunPack branch and reload.", "Switch…",
() => G?.switchBranch?.(), { disabled: !gitOk || !!git?.dirty }));
code.append(actionRow("Update FunPack",
gitOk && git.behind > 0 ? git.behind + " new commit(s) available." : "Pull the latest code and reload.",
gitOk && git.behind > 0 ? git.behind + "↓ Update" : "⬇⟳ Update",
() => G?.update?.(), { disabled: !gitOk || !!git?.dirty }));
wrap.append(code);
wrap.append(el("div", "sw-rows-label", "Pipeline health"));
const health = el("div", "sw-rows");
health.append(infoRow("Workflow template", st.health?.reference_loaded ? "loaded" : "missing", !!st.health?.reference_loaded));
health.append(infoRow("Configured nodes", String(st.health?.configured_slots ?? 0)));
wrap.append(health);
}
const unsub = S ? S.subscribe(render) : null;
render();
if (G?.refresh) G.refresh().then(() => { if (wrap.isConnected) render(); }).catch(() => {});
return () => { if (unsub) unsub(); };
},
});
})();
+55
View File
@@ -1571,3 +1571,58 @@ body.tour-mode { overflow: hidden; }
.sc-pick-prompt { color: var(--muted); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
.sc-pick-count { margin-left: auto; color: var(--faint); font-family: var(--font-mono); font-size: 10.5px; flex-shrink: 0; }
/* ── unified Settings window (macOS System Settings style) ─────────────── */
.sw-overlay { align-items: center; padding: 4vh 16px; }
.settings-win { width: min(960px, 96vw); height: min(660px, 90vh); display: flex;
background: #16140f; border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 24px 64px rgba(0,0,0,.55); overflow: hidden; animation: pop .16s ease; }
.sw-side { width: 218px; flex: 0 0 auto; display: flex; flex-direction: column; gap: 10px;
padding: 12px 10px; background: #121009; border-right: 1px solid var(--line); }
.sw-search { width: 100%; padding: 6px 9px; font: 12.5px var(--font-ui); color: var(--text);
background: var(--ink-2); border: 1px solid var(--line); border-radius: 8px; outline: none; }
.sw-search:focus { border-color: var(--amber); }
.sw-nav { flex: 1; overflow: auto; display: flex; flex-direction: column; gap: 2px; }
.sw-group-title { margin: 10px 6px 3px; font-size: 10px; font-weight: 700; letter-spacing: .8px;
text-transform: uppercase; color: var(--faint); }
.sw-nav-empty { padding: 12px 8px; font-size: 12px; color: var(--faint); }
.sw-item { display: flex; align-items: center; gap: 9px; padding: 5px 8px; border-radius: 7px;
cursor: pointer; font-size: 13px; color: var(--text); user-select: none; }
.sw-item:hover { background: var(--ink-3); }
.sw-item.active { background: linear-gradient(180deg, var(--amber-2), var(--amber)); color: #241503; font-weight: 600; }
.sw-icon { width: 23px; height: 23px; flex: 0 0 auto; border-radius: 6px; display: grid; place-items: center;
background: linear-gradient(180deg,#8a8f98,#5b6068);
box-shadow: inset 0 0 0 .5px rgba(255,255,255,.22), 0 1px 2px rgba(0,0,0,.4); }
.sw-icon svg { display: block; }
.sw-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.sw-head { display: flex; align-items: center; gap: 12px; padding: 13px 18px 11px;
border-bottom: 1px solid var(--line); background: linear-gradient(#1b1813, #161410); }
.sw-head-text { flex: 1; min-width: 0; }
.sw-head-title { font-family: var(--font-display); font-weight: 700; font-size: 17px; letter-spacing: -.01em; }
.sw-head-sub { margin-top: 1px; font-size: 11.5px; color: var(--muted); overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; }
.sw-head-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.sw-close { flex: 0 0 auto; }
.sw-body { flex: 1; overflow: auto; padding: 16px 18px 24px; }
/* grouped macOS-style rows (Refinement & Taste, Updates & ComfyUI) */
.sw-stack { display: flex; flex-direction: column; }
.sw-hint { font-size: 11.5px; line-height: 1.5; color: var(--muted); margin-bottom: 12px; }
.sw-rows-label { margin: 16px 4px 5px; font-size: 10.5px; font-weight: 700; letter-spacing: .6px;
text-transform: uppercase; color: var(--faint); }
.sw-rows { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 10px;
background: var(--ink-2); overflow: hidden; }
.sw-row { display: flex; align-items: center; gap: 12px; padding: 9px 12px; border-top: 1px solid var(--line); }
.sw-row:first-child { border-top: none; }
.sw-row-main { flex: 1; min-width: 0; }
.sw-row-title { font-size: 13px; color: var(--text); }
.sw-row-hint { margin-top: 1px; font-size: 11px; line-height: 1.4; color: var(--muted); }
.sw-row-value { flex: 0 0 auto; max-width: 45%; font-family: var(--font-mono); font-size: 11.5px;
color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-row .btn { flex: 0 0 auto; }
.sw-led { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; }
.sw-led.ok { background: var(--good); box-shadow: 0 0 6px var(--good); }
.sw-led.bad { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
+24 -29
View File
@@ -1,11 +1,11 @@
// Temp files browser: a lightweight media-bin view over ComfyUI's temp directory
// (scene previews & other transient outputs that are wiped on restart). Opened from the
// Settings menu. Each item can be opened in a new tab or saved to disk — no in-app player.
// (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 overlay = null;
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
@@ -20,11 +20,6 @@
function _releaseAllProbes() { [..._probes].forEach(_releaseProbe); }
function close() {
_releaseAllProbes();
if (overlay) { overlay.remove(); overlay = null; }
}
function _fmtSize(n) {
if (!n && n !== 0) return "";
if (n < 1024) return `${n} B`;
@@ -128,32 +123,32 @@
renderBody(body);
}
function open() {
close();
overlay = el("div", "modal-overlay");
const box = el("div", "modal");
const head = el("div", "modal-head");
head.append(el("div", "modal-title", "Temp files"));
const hr = el("div", "modal-head-right");
const countEl = el("span", "pj-meta tmp-count");
const refreshBtn = el("button", "btn ghost tiny", "⟳ Refresh");
const x = el("button", "btn ghost tiny", "✕"); x.onclick = close;
hr.append(countEl, refreshBtn, x);
head.append(hr); box.append(head);
const content = el("div", "modal-content");
content.append(el("div", "es-hint",
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");
content.append(body);
box.append(content);
overlay.append(box);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
document.body.append(overlay);
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.TempBrowserModal = { open, close };
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(),
};
})();
+2 -1
View File
@@ -391,7 +391,7 @@
{
id: "models-modal",
title: "Models dialog",
target: ".modal-overlay .modal",
target: ".settings-win",
body: "Add each loader type (UNet, VAE, CLIP, and so on), pick the node class, then set widget values from dropdowns. Use Wire to when an output must feed another node or a pipeline port.",
pad: 12,
before: () => {
@@ -514,6 +514,7 @@
}
function closeModalOverlay() {
window.SettingsWindow?.close?.(); // settings sections need their cleanup hooks run
document.querySelectorAll(".modal-overlay").forEach((n) => {
if (n.closest?.("#tour-root")) return;
n.remove();