// The media bin: everything this session has produced, and the one place a // result is chosen from. // // Until this existed a second run replaced the first on screen with no way // back -- the picture you were comparing against was simply gone, and the only // record of it was a file on the server nobody in the app could name. So the // bin accumulates across runs while `run.state.images` deliberately does not: // the run's state is one generation, and the bin is the session. // // Three views, because the same twenty results are read three ways: a grid to // compare them, a list to read filenames, icons to see many at once. The view // is the only thing here that is remembered between visits, and it is a // convenience -- a browser refusing storage still gets a working bin. import { composer } from "../composer/composer.js"; import { viewUrl } from "./run.js"; const LS_KEY = "funpack.bin.view"; export const VIEWS = [ { value: "grid", label: "▦", title: "Grid" }, { value: "list", label: "☰", title: "List" }, { value: "icons", label: "⣿", title: "Icons" }, ]; const NAMES = new Set(VIEWS.map((v) => v.value)); const VIDEO = /\.(mp4|webm|mov|mkv|avi)$/i; /** Video and image results arrive the same way and cannot be shown the same way. */ export const kindOf = (file) => (VIDEO.test((file && file.filename) || "") ? "video" : "image"); /** * A file's identity. ComfyUI answers with the same three fields every time, and * the same file can arrive twice -- a reload adopting a finished run replays * its outputs -- so an entry is keyed by the file rather than by arrival. */ export const keyOf = (file = {}) => `${file.type || "output"}|${file.subfolder || ""}|${file.filename || ""}`; function remember(view) { try { window.localStorage.setItem(LS_KEY, view); } catch { /* private mode */ } } export function recallView(fallback = "grid") { try { const raw = window.localStorage.getItem(LS_KEY); return NAMES.has(raw) ? raw : fallback; } catch { return fallback; } } /** * onOpen is given the whole entry plus the URL to fetch it from, so the caller * decides what "open" means -- today it is the viewer, later it will also be * dropping one onto a timeline. */ export function createBin({ onOpen, view = recallView(), persist = true, empty = "Results appear here as you generate." } = {}) { let mode = NAMES.has(view) ? view : "grid"; let items = []; // newest first let selected = null; const seen = new Set(); const host = composer.region.stack({ gap: "sm", label: "Media bin", fill: true }); const control = composer.buttonGroup.md({ label: "Bin view", value: mode, items: VIEWS, onChange: (next) => setView(next), }); let shown = null; // the gallery for the current view // No