feat: app/ui Settings window collects sections that features announce; temp files is the first section
This commit is contained in:
+4
-1
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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] };
|
||||
}
|
||||
Reference in New Issue
Block a user