From 6e74b4532bd8bbb3f7d2003ccdf9a925ffbee076 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:44:09 +0300 Subject: [PATCH] feat: app/ui Settings window collects sections that features announce; temp files is the first section --- app/ui/boot.js | 5 +++- app/ui/features/temp/index.js | 10 +++++++ app/ui/features/temp/view.js | 41 ++++++++++++++++++++++++++++ app/ui/modules.js | 1 + app/ui/shell/settings.js | 51 +++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 app/ui/features/temp/index.js create mode 100644 app/ui/features/temp/view.js create mode 100644 app/ui/shell/settings.js diff --git a/app/ui/boot.js b/app/ui/boot.js index d1490d66..b059ab1d 100644 --- a/app/ui/boot.js +++ b/app/ui/boot.js @@ -1,8 +1,11 @@ // Entry point: build the frame, load the features. Nothing here names a feature. import { build } from "./shell/frame.js"; import { loadFeatures } from "./shell/features.js"; +import { createSettings } from "./shell/settings.js"; +import { hostFor } from "./shell/mounts.js"; import paths from "./modules.js"; const frame = build(document.getElementById("app")); +const settings = createSettings({ menubar: hostFor("menubar") }); const result = await loadFeatures(paths); -window.FunPackUI = { frame, ...result }; +window.FunPackUI = { frame, settings, ...result }; diff --git a/app/ui/features/temp/index.js b/app/ui/features/temp/index.js new file mode 100644 index 00000000..ae7885ac --- /dev/null +++ b/app/ui/features/temp/index.js @@ -0,0 +1,10 @@ +import { mount } from "./view.js"; + +export default { + id: "temp", + mount: "settings", + setup: ({ host }) => host.add({ + id: "temp", title: "Temp files", subtitle: "Where a file went when it did not land in the bin.", + keywords: "temp files output directory", icon: "▥", mount, + }), +}; diff --git a/app/ui/features/temp/view.js b/app/ui/features/temp/view.js new file mode 100644 index 00000000..953b4672 --- /dev/null +++ b/app/ui/features/temp/view.js @@ -0,0 +1,41 @@ +// Where a file went when it did not land in the bin. ComfyUI wipes its temp folder on restart, so +// this is a place to FIND something (opens in a new tab), not to manage it. +import { composer } from "../../composer/composer.js"; + +const ICON = { image: "▦", video: "▶", audio: "♪" }; +const url = (f) => `/view?${new URLSearchParams({ filename: f.filename, subfolder: f.subfolder || "", type: "temp" })}`; +const size = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${Math.round(n / 1024)} KB` : `${(n / 1048576).toFixed(1)} MB`); + +export function mount({ setFooter, close }) { + const body = composer.region.stack({ gap: "sm", fill: true, label: "Temp files" }); + + async function load() { + body.set([composer.hint.default({ text: "Looking…" })]); + let payload; + try { + payload = await (await fetch("/funpack/api/temp", { cache: "no-store" })).json(); + } catch (err) { + body.set([composer.banner.danger({ text: `Could not read the temp directory: ${err.message}` })]); + return; + } + const files = payload.files || []; + const id = (f) => `${f.subfolder}/${f.filename}`; + body.set([files.length + ? composer.gallery.list({ + items: files.map((f) => ({ + id: id(f), label: f.filename, icon: ICON[f.kind] || "▦", badge: f.kind === "image" ? null : f.kind, + hint: `${f.kind} · ${size(Number(f.size) || 0)} · ${new Date((Number(f.mtime) || 0) * 1000).toLocaleTimeString()}`, + thumb: f.kind === "image" ? url(f) : null, + })), + onActivate: (cell) => window.open(url(files.find((f) => id(f) === cell.id)), "_blank", "noopener"), + }) + : composer.emptyState.default({ icon: "▤", title: "Nothing in the temp directory", hint: payload.detail || "" })]); + setFooter({ note: payload.path || "", actions: [ + composer.button.md({ label: "Refresh", onClick: load }), + composer.button.md({ label: "Close", tone: "primary", onClick: close }), + ] }); + } + + load(); + return body; +} diff --git a/app/ui/modules.js b/app/ui/modules.js index 49945661..5b22b21c 100644 --- a/app/ui/modules.js +++ b/app/ui/modules.js @@ -1,4 +1,5 @@ // The one list of UI features. Adding a feature is a folder under ./features and a line here. export default [ "./features/log/index.js", + "./features/temp/index.js", ].map((path) => new URL(path, import.meta.url).href); diff --git a/app/ui/shell/settings.js b/app/ui/shell/settings.js new file mode 100644 index 00000000..83d94366 --- /dev/null +++ b/app/ui/shell/settings.js @@ -0,0 +1,51 @@ +// The Settings window. It owns no section: features announce theirs by mounting at "settings" +// setup({ host }) { host.add({ id, title, subtitle, keywords, icon, tone, mount({ setFooter, close }) -> handle }) } +// and the window lists whatever arrived. It is also the one place a "Settings" button is made. +import { composer } from "../composer/composer.js"; +import { offer } from "./mounts.js"; + +export function createSettings({ menubar }) { + const sections = []; + // childNodes: the feature loader checks a region's children to undo a feature that fails half-way + const host = { childNodes: [], add: (section) => sections.push(section) }; + offer("settings", host); + let current = null; + + function open(id) { + if (current) { current.show(id); return current; } + let shown = null; + const content = composer.region.stack({ gap: "sm", fill: true }); + content.node.classList.add("cx-settings-content"); + + function show(pick) { + const spec = sections.find((s) => s.id === pick) || sections[0]; + if (!spec) return; + shown?.destroy?.(); + modal.setFooter({}); // a section with no footer must not keep the last one's buttons + shown = spec.mount({ setFooter: (f) => modal.setFooter(f), close: () => modal.close("done") }); + content.set([composer.header.md({ text: spec.title }), + spec.subtitle ? composer.hint.default({ text: spec.subtitle }) : null, shown]); + nav.setValue(spec.id); + } + + const nav = composer.filterList.md({ + items: sections.map((s) => ({ id: s.id, label: s.title, icon: s.icon, keywords: s.keywords, tone: s.tone || "neutral" })), + value: id, onChange: show, placeholder: "Search settings", + }); + nav.node.classList.add("cx-filter-full", "cx-settings-rail"); + const body = composer.region.stack({ gap: "none", fill: true, children: [nav, content] }); + body.node.classList.add("cx-settings-body"); + const modal = composer.modal.generic({ + title: "Settings", size: "xl", body, + closeOnOutside: false, // a half-finished edit must not vanish on a stray backdrop click + onClose: () => { shown?.destroy?.(); current = null; }, + }); + modal.node.classList.add("cx-settings-modal"); + current = { show, close: () => modal.close() }; + show(id); + return current; + } + + menubar.append(composer.button.sm({ label: "Settings", onClick: () => open() }).node); + return { open, sections: () => [...sections] }; +}