import { app } from "/scripts/app.js"; import { applyAdaptiveCanvasOnly } from "../shared/nodes2.mjs"; const COMFY_CLASS = "DumasH3LongVideos"; const STATE_PROPERTY = "dumas_h3_longvideos_section_state"; const DOM_WIDGET_NAME = "dumas_h3_longvideos_sections"; const MIN_WIDTH = 520; const MIN_HEIGHT = 280; const GROUPS = [ { id: "prompt", label: "Prompt", defaultCollapsed: false, widgets: ["prompt", "resolution", "megapixels", "beat_split", "anchor_override", "per_beat_length", "shot_seconds", "plan_only", "fps"], }, { id: "refs", label: "Refs", defaultCollapsed: false, widgets: ["ref_mode", "ref_image_size", "ref_noise_aug", "character_memory", "trim_seam", "vary_seed_per_shot", "handoff_offset"], }, { id: "sampling", label: "Sampling", defaultCollapsed: true, widgets: [ "steps", "cfg", "sampler_name", "scheduler", "seed", "apply_model_sampling", "shift_video", "shift_audio", "allow_oversize_shots", "vram_headroom_gb", "allow_res_backoff", "decode_tile_frames", "decode_tile_size", "cleanup_between_shots", ], }, { id: "audio", label: "Audio", defaultCollapsed: true, widgets: [ "global_soundscape", "non_diegetic_music", "auto_soundscape", "auto_silence_nonspeech", "allow_nonspeech_vocals", "mute_nonspeech_audio", "mute_fade_ms", ], }, { id: "scene", label: "Scene Logic", defaultCollapsed: true, widgets: [ "auto_wardrobe", "auto_props", "prevent_nudity", "exposed_terms", "anatomy_guard", "subject_count_guard", "lock_restraints", "contact_guard", "motion_guard", "solidity_guard", ], }, { id: "finish", label: "Upscale/Detail", defaultCollapsed: true, widgets: [ "upscale", "upscale_model", "upscale_target_short_edge", "upscale_batch", "detail_pass", "detail_sampler_name", "detail_scheduler", "detail_steps", "detail_denoise", ], }, { id: "overlay", label: "Overlays", defaultCollapsed: true, widgets: [ "watermark_text", "watermark_position", "watermark_size", "watermark_opacity", "watermark_margin", "intro_text", "intro_position", "intro_seconds", "intro_fade", "intro_size", "overlay_font", "overlay_stroke", ], }, ]; function injectCSS() { if (document.getElementById("dumas-h3lv-sections-css")) return; const style = document.createElement("style"); style.id = "dumas-h3lv-sections-css"; style.textContent = ` .dh3lv-sections { box-sizing: border-box; width: 100%; padding: 8px 10px 6px; color: #e6e7eb; font: 12px/1.35 "Segoe UI", sans-serif; pointer-events: auto; background: linear-gradient(180deg, rgba(33, 36, 42, 0.96), rgba(22, 24, 29, 0.96)); border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .dh3lv-sections-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; } .dh3lv-sections-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #9da5b1; } .dh3lv-sections-actions { display: flex; gap: 6px; } .dh3lv-sections-list { display: flex; flex-wrap: wrap; gap: 6px; } .dh3lv-chip, .dh3lv-action { appearance: none; border: 1px solid #464d59; background: #262c35; color: #d7dce3; border-radius: 999px; padding: 5px 9px; cursor: pointer; font: inherit; line-height: 1.1; } .dh3lv-chip[data-open="true"] { background: #d96f2b; border-color: #f09358; color: #fff7f0; } .dh3lv-chip:hover, .dh3lv-action:hover { filter: brightness(1.06); } .dh3lv-count { opacity: 0.78; margin-left: 4px; font-size: 11px; } `; document.head.appendChild(style); } function defaultState() { const state = {}; for (const group of GROUPS) state[group.id] = !group.defaultCollapsed; return state; } function parseState(value) { let parsed = value; if (typeof parsed === "string") { try { parsed = JSON.parse(parsed); } catch (_error) { parsed = null; } } const base = defaultState(); if (!parsed || typeof parsed !== "object") return base; for (const group of GROUPS) { if (typeof parsed[group.id] === "boolean") base[group.id] = parsed[group.id]; } return base; } function readState(node) { return parseState(node.properties?.[STATE_PROPERTY] || node._dh3lvSectionState || ""); } function writeState(node, state) { const normalized = parseState(state); const serialized = JSON.stringify(normalized); node._dh3lvSectionState = serialized; node.properties = node.properties || {}; node.properties[STATE_PROPERTY] = serialized; } function findWidget(node, name) { return (node.widgets || []).find((widget) => widget?.name === name) || null; } function setWidgetHidden(widget, hidden) { if (!widget) return; if (!widget._dh3lvOriginal) { widget._dh3lvOriginal = { type: widget.type, computeSize: widget.computeSize, hidden: widget.hidden, }; } if (hidden) { widget.type = "hidden"; widget.hidden = true; widget.computeSize = () => [0, -4]; return; } widget.type = widget._dh3lvOriginal.type; widget.hidden = !!widget._dh3lvOriginal.hidden; widget.computeSize = widget._dh3lvOriginal.computeSize; } function applyVisibility(node) { const state = readState(node); for (const group of GROUPS) { for (const name of group.widgets) { const widget = findWidget(node, name); if (!widget || widget.name === DOM_WIDGET_NAME) continue; setWidgetHidden(widget, !state[group.id]); } } } function resizeNode(node) { requestAnimationFrame(() => { const size = node.computeSize?.(); if (Array.isArray(size)) { node.size[0] = Math.max(MIN_WIDTH, size[0] || 0, node.size?.[0] || 0); node.size[1] = Math.max(MIN_HEIGHT, size[1] || 0); } node.setDirtyCanvas?.(true, true); }); } function renderToolbar(node) { const ui = node._dh3lvUI; if (!ui) return; const state = readState(node); ui.list.innerHTML = ""; for (const group of GROUPS) { const button = document.createElement("button"); button.type = "button"; button.className = "dh3lv-chip"; button.dataset.open = state[group.id] ? "true" : "false"; button.textContent = state[group.id] ? `Hide ${group.label}` : `Show ${group.label}`; const count = document.createElement("span"); count.className = "dh3lv-count"; count.textContent = String(group.widgets.filter((name) => findWidget(node, name)).length); button.appendChild(count); button.addEventListener("click", () => { const next = readState(node); next[group.id] = !next[group.id]; writeState(node, next); applyVisibility(node); renderToolbar(node); resizeNode(node); }); ui.list.appendChild(button); } } function setAll(node, open) { const next = {}; for (const group of GROUPS) next[group.id] = !!open; writeState(node, next); applyVisibility(node); renderToolbar(node); resizeNode(node); } function setupNode(node) { if (node._dh3lvUI) return; injectCSS(); writeState(node, readState(node)); const root = document.createElement("div"); root.className = "dh3lv-sections"; const head = document.createElement("div"); head.className = "dh3lv-sections-head"; const title = document.createElement("div"); title.className = "dh3lv-sections-title"; title.textContent = "Sections"; const actions = document.createElement("div"); actions.className = "dh3lv-sections-actions"; const expandAll = document.createElement("button"); expandAll.type = "button"; expandAll.className = "dh3lv-action"; expandAll.textContent = "Expand All"; expandAll.addEventListener("click", () => setAll(node, true)); const collapseAll = document.createElement("button"); collapseAll.type = "button"; collapseAll.className = "dh3lv-action"; collapseAll.textContent = "Collapse Extras"; collapseAll.addEventListener("click", () => { const next = defaultState(); writeState(node, next); applyVisibility(node); renderToolbar(node); resizeNode(node); }); actions.append(expandAll, collapseAll); head.append(title, actions); const list = document.createElement("div"); list.className = "dh3lv-sections-list"; root.append(head, list); node._dh3lvUI = { root, list }; const widget = node.addDOMWidget(DOM_WIDGET_NAME, "custom", root, { getValue: () => null, setValue: () => {}, serialize: false, getMinHeight: () => 52, hideOnZoom: false, }); applyAdaptiveCanvasOnly(widget); const widgets = node.widgets || []; const index = widgets.indexOf(widget); if (index > 0) { widgets.splice(index, 1); widgets.unshift(widget); } applyVisibility(node); renderToolbar(node); resizeNode(node); } app.registerExtension({ name: "Dumas.H3LongVideosSections", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData?.name !== COMFY_CLASS) return; const originalOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function onNodeCreated() { const result = originalOnNodeCreated?.apply(this, arguments); setupNode(this); return result; }; const originalConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function onConfigure() { const result = originalConfigure?.apply(this, arguments); setupNode(this); writeState(this, readState(this)); applyVisibility(this); renderToolbar(this); resizeNode(this); return result; }; const originalSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function onSerialize(o) { writeState(this, readState(this)); const result = originalSerialize?.apply(this, arguments); if (o && this.properties?.[STATE_PROPERTY]) { o.properties = o.properties || {}; o.properties[STATE_PROPERTY] = this.properties[STATE_PROPERTY]; } return result; }; }, });