Files
MajoorWaldi-ComfyUI-Majoor-…/js/components/sidebar/SidebarView.js
T
MajoorWaldi 2fa41a0e76 feat: add useGridState and useVirtualGrid composables for asset management
- Implemented useGridState to manage grid state, including asset selection and loading states.
- Created useVirtualGrid to centralize virtualization helpers for improved performance.
- Added createVueApp for isolated Vue 3 + Pinia application instances.
- Introduced Vite configuration for building the ComfyUI-Majoor-AssetsManager with external dependencies.
2026-04-03 11:04:55 +02:00

226 lines
8.9 KiB
JavaScript

/**
* Inline Asset Sidebar - Integrates into the Assets Manager panel
*
* This module keeps the public API stable (`showAssetInSidebar`, `closeSidebar`)
* while delegating UI rendering to Vue components via `useActiveAsset` (Phase 5).
*/
import { getAssetMetadata, getFileMetadataScoped, getFolderInfo } from "../../api/client.js";
import { loadMajoorSettings } from "../../app/settings.js";
import {
setActiveAsset,
clearActiveAsset,
patchActiveAsset,
} from "../../vue/composables/useActiveAsset.js";
const SIDEBAR_OPEN_WIDTH_PX = 360;
const SIDEBAR_MIN_WIDTH_PX = 240;
const SIDEBAR_MAX_WIDTH_PX = 640;
function _sidebarWidthFromSettings() {
try {
const settings = loadMajoorSettings();
const raw = Number(settings?.sidebar?.widthPx);
if (!Number.isFinite(raw)) return SIDEBAR_OPEN_WIDTH_PX;
return Math.max(SIDEBAR_MIN_WIDTH_PX, Math.min(SIDEBAR_MAX_WIDTH_PX, Math.round(raw)));
} catch {
return SIDEBAR_OPEN_WIDTH_PX;
}
}
function _applySidebarOpenState(sidebar, open) {
if (!sidebar) return;
const isLeft = String(sidebar?.dataset?.position || "right").toLowerCase() === "left";
const borderColor = "var(--mjr-border, rgba(255,255,255,0.12))";
if (open) {
const w = `${_sidebarWidthFromSettings()}px`;
sidebar.style.flex = `0 0 ${w}`;
sidebar.style.width = w;
sidebar.style.maxWidth = w;
sidebar.style.minWidth = "0";
sidebar.style.overflow = "hidden";
sidebar.style.borderLeft = isLeft ? "none" : `1px solid ${borderColor}`;
sidebar.style.borderRight = isLeft ? `1px solid ${borderColor}` : "none";
} else {
sidebar.style.flex = "0 0 0px";
sidebar.style.width = "0";
sidebar.style.maxWidth = "0";
sidebar.style.minWidth = "0";
sidebar.style.overflow = "hidden";
sidebar.style.borderLeft = "none";
sidebar.style.borderRight = "none";
}
}
// ─── metadata helpers ─────────────────────────────────────────────────────────
function _hasMeaningfulMetadataRaw(value) {
if (value == null) return false;
let obj = value;
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed || trimmed === "{}" || trimmed === "null") return false;
try { obj = JSON.parse(trimmed); } catch { return false; }
}
if (typeof obj === "object") {
try {
if (obj.geninfo || obj.prompt || obj.workflow) return true;
if (obj.metadata_raw && typeof obj.metadata_raw === "object") {
return Boolean(obj.metadata_raw.geninfo || obj.metadata_raw.prompt || obj.metadata_raw.workflow);
}
return false;
} catch { return false; }
}
return false;
}
function _hasGenerationLikeData(obj) {
if (!obj) return false;
return !!(obj.geninfo || obj.prompt || _hasMeaningfulMetadataRaw(obj.metadata_raw));
}
/**
* Show asset in inline sidebar.
*
* Phase 5: DOM building is now owned by Vue (SidebarSection.vue →
* AssetSidebarContent.vue). This function only:
* 1. Applies CSS open-state to the sidebar element (imperative, unchanged).
* 2. Calls setActiveAsset(asset, onUpdate) to trigger Vue re-render.
* 3. Sets backward-compat properties (_currentAsset, _currentFullAsset) that
* sidebarController.js still reads.
* 4. Fires loadMetadataAsync which calls patchActiveAsset() with enrichment.
*/
export async function showAssetInSidebar(sidebar, asset, onUpdate) {
if (!sidebar || !asset) return;
const isFolderAsset = String(asset?.kind || "").toLowerCase() === "folder";
try {
if (sidebar._closeTimer) {
clearTimeout(sidebar._closeTimer);
sidebar._closeTimer = null;
}
} catch (e) {
console.debug?.(e);
}
const requestSeq = (sidebar._requestSeq = (sidebar._requestSeq || 0) + 1);
sidebar._currentFetchAbortController?.abort?.();
sidebar._currentFetchAbortController = null;
// Open sidebar CSS (still imperative — Vue owns content, not layout).
sidebar.classList.add("is-open");
_applySidebarOpenState(sidebar, true);
// Backward-compat properties read by sidebarController.js.
sidebar._currentAsset = asset;
sidebar._currentFullAsset = asset;
sidebar._ratingTagsSection = null;
// Trigger Vue re-render with the base asset.
setActiveAsset(asset, onUpdate);
// ── Async metadata enrichment ─────────────────────────────────────────────
const buildFetchOptions = () => {
sidebar._currentFetchAbortController?.abort?.();
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
sidebar._currentFetchAbortController = controller;
return controller?.signal ? { signal: controller.signal } : {};
};
const applyUpdate = (extra = {}) => {
if (sidebar._requestSeq !== requestSeq || sidebar._currentAsset !== asset) return;
const updated = { ...(sidebar._currentFullAsset ?? asset), ...extra };
sidebar._currentFullAsset = updated;
patchActiveAsset(extra, onUpdate);
};
const loadMetadataAsync = async () => {
if (sidebar._requestSeq !== requestSeq || sidebar._currentAsset !== asset) return;
const opts = buildFetchOptions();
const signal = opts.signal;
if (isFolderAsset) {
try {
const filepath = String(asset?.filepath || asset?.subfolder || "").trim();
const root_id = String(asset?.root_id || asset?.rootId || "").trim();
const subfolder = root_id ? String(asset?.subfolder || "").trim() : "";
const res = await getFolderInfo({ filepath, root_id, subfolder }, opts);
if (!signal?.aborted && res?.ok && res.data) applyUpdate({ folder_info: res.data });
} catch (err) {
if (!signal?.aborted) console.warn("Failed to load folder details:", err);
} finally {
if (!signal?.aborted) sidebar._currentFetchAbortController = null;
}
return;
}
const current = () => sidebar._currentFullAsset ?? asset;
try {
if (asset.id && !_hasGenerationLikeData(current()) && !current().exif) {
const result = await getAssetMetadata(asset.id, opts);
if (signal?.aborted) return;
if (result?.ok && result.data) applyUpdate(result.data);
}
} catch (err) {
if (!signal?.aborted) console.warn("Failed to load full asset metadata:", err);
}
if (signal?.aborted) return;
if (!_hasGenerationLikeData(current())) {
const filename = String(current()?.filename || "").trim();
const type = String(current()?.type || "output").trim().toLowerCase();
const subfolder = String(current()?.subfolder || "").trim();
const root_id = String(current()?.root_id || current()?.rootId || "").trim();
if (filename) {
try {
const result = await getFileMetadataScoped({ type, filename, subfolder, root_id }, opts);
if (signal?.aborted) return;
if (result?.ok && result.data) {
const md = result.data;
applyUpdate({
prompt: current().prompt ?? md.prompt,
workflow: current().workflow ?? md.workflow,
geninfo: current().geninfo ?? md.geninfo,
exif: current().exif ?? md.exif,
ffprobe: current().ffprobe ?? md.ffprobe,
metadata_raw: current().metadata_raw ?? md,
});
}
} catch (err) {
if (!signal?.aborted) console.warn("Failed to load scoped metadata:", err);
}
}
}
if (!signal?.aborted) sidebar._currentFetchAbortController = null;
};
void loadMetadataAsync();
}
/**
* Close inline sidebar
*/
export function closeSidebar(sidebar) {
if (!sidebar) return;
sidebar._requestSeq = (sidebar._requestSeq || 0) + 1;
sidebar.classList.remove("is-open");
_applySidebarOpenState(sidebar, false);
sidebar._currentAsset = null;
sidebar._currentFullAsset = null;
sidebar._currentFetchAbortController?.abort?.();
sidebar._currentFetchAbortController = null;
// Tell Vue to clear the sidebar content.
clearActiveAsset();
// Dispatch event so controllers can update their state.
try {
sidebar.dispatchEvent?.(new CustomEvent("mjr:sidebar-closed", { bubbles: true }));
} catch (e) {
console.debug?.(e);
}
}
// Raw metadata is now available as a toggle inside WorkflowMinimapSection.