535 lines
24 KiB
JavaScript
535 lines
24 KiB
JavaScript
/** Curated material snapshots: generated image + exact workflow + node blocks. */
|
||
|
||
import { app } from '../../../scripts/app.js';
|
||
import { translate } from './locales.js';
|
||
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
|
||
import { showImageWorkbench } from './ui_gallery_detail.js';
|
||
import { text, sectionLabel, fileBaseName, jsonResponse, renderDetailedNodeCards } from './material_inspector.js';
|
||
|
||
const t = (key, params) => translate(key, params);
|
||
|
||
function materialAssetUrl(filename, asset) {
|
||
if (!filename || !asset) return '';
|
||
return `/anomalous/material_asset?filename=${encodeURIComponent(filename)}&asset=${encodeURIComponent(asset)}`;
|
||
}
|
||
|
||
function hideSiblingWorkspaceViews(owner) {
|
||
if (owner.notebookBody) owner.notebookBody.style.display = 'none';
|
||
if (owner.recipeView) owner.recipeView.style.display = 'none';
|
||
owner.notebookNotesTab?.classList.remove('active');
|
||
owner.notebookRecipesTab?.classList.remove('active');
|
||
owner.notebookMaterialsTab?.classList.add('active');
|
||
}
|
||
|
||
async function fetchMaterial(filename, options = {}) {
|
||
const response = await fetch(`/anomalous/material_full?filename=${encodeURIComponent(filename)}`, {
|
||
cache: 'no-store',
|
||
signal: options.signal,
|
||
});
|
||
const payload = await jsonResponse(response, 'material load failed');
|
||
if (payload.status !== 'success') throw new Error(payload.message || 'material load failed');
|
||
return payload;
|
||
}
|
||
|
||
function renderMaterialInspector(content, payload) {
|
||
const references = Array.isArray(payload.data?.model_references) ? payload.data.model_references : [];
|
||
const blocks = Array.isArray(payload.node_blocks) ? payload.node_blocks : [];
|
||
|
||
const overview = document.createElement('div');
|
||
overview.className = 'anomalous-library-detail-stats';
|
||
const nodeStat = document.createElement('div');
|
||
nodeStat.className = 'anomalous-library-detail-stat';
|
||
text(nodeStat, 'strong', blocks.length);
|
||
text(nodeStat, 'span', t('materialParameterNodes'));
|
||
const modelStat = document.createElement('div');
|
||
modelStat.className = 'anomalous-library-detail-stat';
|
||
text(modelStat, 'strong', references.length);
|
||
text(modelStat, 'span', t('materialModelReferences'));
|
||
overview.append(nodeStat, modelStat);
|
||
content.appendChild(overview);
|
||
|
||
if (payload.data?.selection?.scope === 'nodes') {
|
||
text(content, 'p', t('materialUseFromNodeAssistant'), 'anomalous-library-detail-callout');
|
||
}
|
||
|
||
if (references.length) {
|
||
sectionLabel(content, t('materialModelReferences'));
|
||
const models = document.createElement('div');
|
||
models.className = 'anomalous-material-expanded-models';
|
||
for (const reference of references) {
|
||
const item = document.createElement('div');
|
||
item.className = 'anomalous-material-expanded-model';
|
||
const rawValue = reference.saved_value || reference.name || t('materialUntitled');
|
||
const name = text(item, 'span', fileBaseName(rawValue));
|
||
name.title = String(rawValue);
|
||
text(item, 'small', reference.category || 'model');
|
||
models.appendChild(item);
|
||
}
|
||
content.appendChild(models);
|
||
}
|
||
|
||
const blocksHeader = document.createElement('div');
|
||
blocksHeader.className = 'anomalous-material-blocks-header';
|
||
sectionLabel(blocksHeader, t('materialDetailedNodeParameters', { count: blocks.length }));
|
||
|
||
if (blocks.length > 1) {
|
||
let allOpen = true;
|
||
const toggleAllBtn = document.createElement('button');
|
||
toggleAllBtn.type = 'button';
|
||
toggleAllBtn.className = 'anomalous-material-toggle-all-btn';
|
||
toggleAllBtn.textContent = t('materialCollapseAll') || '全部收起';
|
||
toggleAllBtn.onclick = () => {
|
||
allOpen = !allOpen;
|
||
content.querySelectorAll('.anomalous-material-node-detail').forEach(d => {
|
||
d.open = allOpen;
|
||
});
|
||
toggleAllBtn.textContent = allOpen
|
||
? (t('materialCollapseAll') || '全部收起')
|
||
: (t('materialExpandAllNodes') || '全部展开');
|
||
};
|
||
blocksHeader.appendChild(toggleAllBtn);
|
||
}
|
||
content.appendChild(blocksHeader);
|
||
|
||
if (blocks.length) {
|
||
renderDetailedNodeCards(content, blocks, { defaultOpen: true });
|
||
} else {
|
||
text(content, 'p', t('materialNoNodeParameters'), 'anomalous-material-muted');
|
||
}
|
||
}
|
||
|
||
function leaveMaterialDetail(owner) {
|
||
owner.materialDetailController?.abort();
|
||
owner.materialDetailController = null;
|
||
owner.materialDetailView?.remove();
|
||
owner.materialDetailView = null;
|
||
if (owner.materialIntro) owner.materialIntro.style.display = 'flex';
|
||
if (owner.materialList) owner.materialList.style.display = 'grid';
|
||
if (owner.materialToolbar) owner.materialToolbar.style.display = 'flex';
|
||
if (owner.materialPager) owner.materialPager.style.display = 'flex';
|
||
}
|
||
|
||
async function deleteMaterial(owner, material) {
|
||
if (!await anomalousConfirm(t('materialDeleteConfirm'))) return false;
|
||
const response = await fetch('/anomalous/delete_material', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ filename: material.filename }),
|
||
});
|
||
if (!response.ok) {
|
||
await anomalousAlert(t('materialDeleteError'));
|
||
return false;
|
||
}
|
||
leaveMaterialDetail(owner);
|
||
await owner.refreshMaterials?.();
|
||
return true;
|
||
}
|
||
|
||
async function openMaterialWorkflow(owner, filename) {
|
||
const payload = await fetchMaterial(filename);
|
||
if (!payload.data?.workflow || typeof app.loadGraphData !== 'function') throw new Error('material workflow unavailable');
|
||
await app.loadGraphData(JSON.parse(JSON.stringify(payload.data.workflow)));
|
||
app.canvas?.setDirty?.(true, true);
|
||
owner.closeWorkspace?.();
|
||
owner.close?.();
|
||
window.setTimeout(() => window.anomalous_resolve_all_missing_nodes?.(true, false), 0);
|
||
}
|
||
|
||
function buildMaterialDetailHeader(owner, material) {
|
||
const header = document.createElement('header');
|
||
header.className = 'anomalous-library-detail-header';
|
||
|
||
const back = text(header, 'button', `← ${t('materialBackToLibrary')}`, 'anomalous-library-detail-back');
|
||
back.type = 'button';
|
||
back.onclick = () => leaveMaterialDetail(owner);
|
||
|
||
const heading = document.createElement('div');
|
||
heading.className = 'anomalous-library-detail-heading';
|
||
const title = text(heading, 'h2', material.name || t('materialUntitled'));
|
||
title.title = material.name || t('materialUntitled');
|
||
text(
|
||
heading,
|
||
'span',
|
||
material.selection?.scope === 'nodes' ? t('materialSelectedNodeMaterial') : t('materialFullWorkflowMaterial'),
|
||
'anomalous-material-scope-badge',
|
||
);
|
||
header.appendChild(heading);
|
||
|
||
const headerActions = document.createElement('div');
|
||
headerActions.className = 'anomalous-library-detail-actions';
|
||
if ((material.capabilities || []).includes('open_workflow')) {
|
||
const open = text(headerActions, 'button', `🚀 ${t('materialOpenWorkflow')}`, 'anomalous-btn-primary');
|
||
open.type = 'button';
|
||
open.onclick = async () => {
|
||
open.disabled = true;
|
||
try {
|
||
await openMaterialWorkflow(owner, material.filename);
|
||
} catch (error) {
|
||
console.error('Could not open material workflow:', error);
|
||
await anomalousAlert(t('materialOpenError'));
|
||
open.disabled = false;
|
||
}
|
||
};
|
||
}
|
||
const edit = text(headerActions, 'button', t('materialEditDetails'), 'anomalous-btn-ghost');
|
||
edit.type = 'button';
|
||
edit.onclick = () => toggleMaterialEditor(owner, material, header);
|
||
const remove = text(headerActions, 'button', `🗑️ ${t('materialDelete')}`, 'anomalous-btn-ghost');
|
||
remove.type = 'button';
|
||
remove.onclick = () => deleteMaterial(owner, material);
|
||
header.appendChild(headerActions);
|
||
|
||
return header;
|
||
}
|
||
|
||
function buildMaterialMediaStage(owner, material, sourceNameElement) {
|
||
const media = document.createElement('aside');
|
||
media.className = 'anomalous-library-detail-media';
|
||
const previewUrl = materialAssetUrl(material.filename, material.image?.preview_asset_id || material.image?.source_asset_id);
|
||
const sourceUrl = materialAssetUrl(material.filename, material.image?.source_asset_id || material.image?.preview_asset_id);
|
||
|
||
const imageStage = document.createElement('button');
|
||
imageStage.className = 'anomalous-library-detail-image-stage';
|
||
imageStage.type = 'button';
|
||
imageStage.title = t('materialOpenSourceImage');
|
||
|
||
if (previewUrl) {
|
||
const image = document.createElement('img');
|
||
image.src = previewUrl;
|
||
image.alt = material.name || t('materialUntitled');
|
||
image.decoding = 'async';
|
||
imageStage.appendChild(image);
|
||
imageStage.onclick = () => owner.showGalleryViewer?.(sourceUrl);
|
||
} else {
|
||
imageStage.textContent = '🖼️';
|
||
imageStage.disabled = true;
|
||
}
|
||
media.appendChild(imageStage);
|
||
|
||
const sourceMeta = document.createElement('div');
|
||
sourceMeta.className = 'anomalous-library-detail-source';
|
||
const sourceCopy = document.createElement('div');
|
||
sourceCopy.className = 'anomalous-library-detail-source-copy';
|
||
text(sourceCopy, 'span', t('materialSourceImage'));
|
||
sourceCopy.appendChild(sourceNameElement);
|
||
sourceMeta.appendChild(sourceCopy);
|
||
|
||
const openSource = text(sourceMeta, 'button', t('materialOpenSourceImage'), 'anomalous-library-detail-source-open');
|
||
openSource.type = 'button';
|
||
openSource.disabled = !sourceUrl;
|
||
openSource.onclick = () => owner.showGalleryViewer?.(sourceUrl);
|
||
sourceMeta.appendChild(openSource);
|
||
|
||
media.appendChild(sourceMeta);
|
||
return media;
|
||
}
|
||
|
||
async function showMaterialDetail(owner, material) {
|
||
owner.materialDetailController?.abort();
|
||
owner.materialDetailView?.remove();
|
||
owner.materialDetailView = null;
|
||
if (owner.materialIntro) owner.materialIntro.style.display = 'none';
|
||
if (owner.materialList) owner.materialList.style.display = 'none';
|
||
if (owner.materialToolbar) owner.materialToolbar.style.display = 'none';
|
||
if (owner.materialPager) owner.materialPager.style.display = 'none';
|
||
|
||
const detail = document.createElement('section');
|
||
detail.className = 'anomalous-library-detail-view';
|
||
owner.materialDetailView = detail;
|
||
|
||
const sourceNameElement = document.createElement('strong');
|
||
sourceNameElement.textContent = t('loading');
|
||
|
||
const header = buildMaterialDetailHeader(owner, material);
|
||
detail.appendChild(header);
|
||
|
||
const layout = document.createElement('div');
|
||
layout.className = 'anomalous-library-detail-layout';
|
||
|
||
const media = buildMaterialMediaStage(owner, material, sourceNameElement);
|
||
layout.appendChild(media);
|
||
|
||
const inspector = document.createElement('main');
|
||
inspector.className = 'anomalous-library-detail-inspector';
|
||
text(inspector, 'p', t('loading'), 'anomalous-material-muted');
|
||
layout.appendChild(inspector);
|
||
|
||
detail.appendChild(layout);
|
||
owner.materialView.appendChild(detail);
|
||
|
||
const controller = new AbortController();
|
||
owner.materialDetailController = controller;
|
||
try {
|
||
const payload = await fetchMaterial(material.filename, { signal: controller.signal });
|
||
if (owner.materialDetailView !== detail) return;
|
||
const sourceImage = payload.data?.source?.image || {};
|
||
const sourceName = sourceImage.filename || material.name || t('materialUntitled');
|
||
sourceNameElement.textContent = sourceName;
|
||
sourceNameElement.title = sourceName;
|
||
inspector.replaceChildren();
|
||
renderMaterialInspector(inspector, payload);
|
||
} catch (error) {
|
||
if (error?.name === 'AbortError') return;
|
||
console.error('Could not load material detail:', error);
|
||
inspector.replaceChildren();
|
||
text(inspector, 'p', t('materialDetailLoadError'), 'anomalous-material-empty');
|
||
} finally {
|
||
if (owner.materialDetailController === controller) owner.materialDetailController = null;
|
||
}
|
||
}
|
||
|
||
function renderMaterialCard(owner, material) {
|
||
const card = document.createElement('article');
|
||
card.className = 'anomalous-material-card';
|
||
card.title = t('materialViewDetails') || '点击查看详细参数';
|
||
card.onclick = () => showMaterialDetail(owner, material);
|
||
card.tabIndex = 0;
|
||
card.setAttribute('aria-label', `${material.name} — ${t('materialViewDetails')}`);
|
||
card.onkeydown = event => {
|
||
if (event.target === card && ['Enter', ' '].includes(event.key)) {
|
||
event.preventDefault();
|
||
showMaterialDetail(owner, material);
|
||
}
|
||
};
|
||
|
||
const remove = document.createElement('button');
|
||
remove.type = 'button';
|
||
remove.className = 'anomalous-material-card-delete';
|
||
remove.innerHTML = '🗑️';
|
||
remove.title = t('materialDelete');
|
||
remove.onclick = (e) => {
|
||
e.stopPropagation();
|
||
deleteMaterial(owner, material);
|
||
};
|
||
card.appendChild(remove);
|
||
|
||
if ((material.capabilities || []).includes('open_workflow')) {
|
||
const quickOpen = document.createElement('button');
|
||
quickOpen.type = 'button';
|
||
quickOpen.className = 'anomalous-material-card-quick-load';
|
||
quickOpen.innerHTML = '🚀';
|
||
quickOpen.title = t('materialOpenWorkflow');
|
||
quickOpen.onclick = async (e) => {
|
||
e.stopPropagation();
|
||
try {
|
||
await openMaterialWorkflow(owner, material.filename);
|
||
} catch (error) {
|
||
await anomalousAlert(t('materialOpenError'));
|
||
}
|
||
};
|
||
card.appendChild(quickOpen);
|
||
}
|
||
|
||
const preview = document.createElement('div');
|
||
preview.className = 'anomalous-material-card-preview';
|
||
const previewUrl = materialAssetUrl(material.filename, material.image?.preview_asset_id || material.image?.source_asset_id);
|
||
if (previewUrl) {
|
||
const image = document.createElement('img');
|
||
image.src = previewUrl;
|
||
image.alt = material.name || t('materialUntitled');
|
||
image.loading = 'lazy';
|
||
preview.appendChild(image);
|
||
} else {
|
||
preview.textContent = '🖼️';
|
||
}
|
||
card.appendChild(preview);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'anomalous-material-card-body';
|
||
const cardTitle = text(body, 'h3', material.name || t('materialUntitled'));
|
||
cardTitle.title = material.name || t('materialUntitled');
|
||
|
||
const metadata = document.createElement('div');
|
||
metadata.className = 'anomalous-material-card-meta';
|
||
if (material.selection?.scope === 'nodes') {
|
||
const isPromptOnly = Array.isArray(material.node_types) &&
|
||
material.node_types.length > 0 &&
|
||
material.node_types.every(tp => /cliptextencode/i.test(tp));
|
||
const badgeText = isPromptOnly
|
||
? `💬 ${t('materialPromptNode') || '提示词'}`
|
||
: t('materialSelectedNodeMaterial');
|
||
text(metadata, 'span', badgeText, 'anomalous-material-scope-badge is-nodes');
|
||
} else {
|
||
text(metadata, 'span', t('materialFullWorkflowMaterial'), 'anomalous-material-scope-badge is-workflow');
|
||
}
|
||
text(metadata, 'span', t('materialNodeSummary', { count: material.node_count || 0 }), 'anomalous-material-meta-pill');
|
||
body.appendChild(metadata);
|
||
|
||
if (Array.isArray(material.node_types) && material.node_types.length) {
|
||
text(body, 'small', material.node_types.slice(0, 5).join(' · '), 'anomalous-material-types');
|
||
}
|
||
if (material.tags?.length) {
|
||
const tags = text(body, 'div', '', 'anomalous-material-tags');
|
||
material.tags.forEach(tag => text(tags, 'span', tag, 'anomalous-material-tag'));
|
||
}
|
||
card.appendChild(body);
|
||
|
||
return card;
|
||
}
|
||
|
||
function toggleMaterialEditor(owner, material, header) {
|
||
const existing = header.querySelector('.anomalous-material-editor');
|
||
if (existing) { existing.remove(); return; }
|
||
const form = text(header, 'form', '', 'anomalous-material-editor');
|
||
const nameLabel = text(form, 'label', t('materialName'));
|
||
const name = text(nameLabel, 'input', '');
|
||
name.value = material.name || '';
|
||
name.maxLength = 120;
|
||
name.required = true;
|
||
const tagsLabel = text(form, 'label', t('materialTags'));
|
||
const tags = text(tagsLabel, 'input', '');
|
||
tags.value = (material.tags || []).join(', ');
|
||
tags.placeholder = t('materialTagsHint');
|
||
tags.maxLength = 1200;
|
||
const save = text(form, 'button', t('materialSaveDetails'), 'anomalous-btn-primary');
|
||
save.type = 'submit';
|
||
const status = text(form, 'span', '', 'anomalous-material-muted');
|
||
status.setAttribute('role', 'status');
|
||
form.onsubmit = async event => {
|
||
event.preventDefault();
|
||
save.disabled = true;
|
||
try {
|
||
const response = await fetch('/anomalous/update_material', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ filename: material.filename, name: name.value.trim(),
|
||
tags: tags.value.split(/[,,]/).map(value => value.trim()).filter(Boolean) }),
|
||
});
|
||
const payload = await jsonResponse(response, 'material update failed');
|
||
if (payload.status !== 'success') throw new Error('material update failed');
|
||
Object.assign(material, payload.material);
|
||
header.querySelector('h2').textContent = material.name;
|
||
header.querySelector('h2').title = material.name;
|
||
const preview = owner.materialDetailView?.querySelector('.anomalous-library-detail-image-stage img');
|
||
if (preview) preview.alt = material.name;
|
||
await owner.refreshMaterials?.();
|
||
form.remove();
|
||
} catch (error) {
|
||
status.textContent = t('materialUpdateError');
|
||
save.disabled = false;
|
||
}
|
||
};
|
||
name.focus();
|
||
}
|
||
|
||
function buildMaterialFilters(owner) {
|
||
const toolbar = text(owner.materialView, 'div', '', 'anomalous-material-toolbar');
|
||
owner.materialToolbar = toolbar;
|
||
const search = text(toolbar, 'input', '');
|
||
search.type = 'search';
|
||
search.placeholder = t('materialSearchHint');
|
||
search.setAttribute('aria-label', t('materialSearchHint'));
|
||
search.maxLength = 200;
|
||
search.value = owner.materialQuery || '';
|
||
search.oninput = () => {
|
||
clearTimeout(owner.materialSearchTimer);
|
||
owner.materialQuery = search.value;
|
||
owner.materialSearchTimer = setTimeout(() => owner.refreshMaterials(1), 250);
|
||
};
|
||
const kind = text(toolbar, 'select', '');
|
||
kind.setAttribute('aria-label', t('materialFilterKind'));
|
||
for (const [value, key] of [['', 'materialAllKinds'], ['image_workflow_snapshot', 'materialFullWorkflowMaterial'], ['image_node_selection', 'materialSelectedNodeMaterial']]) {
|
||
const option = text(kind, 'option', t(key));
|
||
option.value = value;
|
||
}
|
||
kind.value = owner.materialKind || '';
|
||
kind.onchange = () => { owner.materialKind = kind.value; owner.refreshMaterials(1); };
|
||
owner.materialTagSelect = text(toolbar, 'select', '');
|
||
owner.materialTagSelect.setAttribute('aria-label', t('materialTags'));
|
||
owner.materialTagSelect.onchange = () => { owner.materialTag = owner.materialTagSelect.value; owner.refreshMaterials(1); };
|
||
}
|
||
|
||
function renderMaterialPagination(owner, payload) {
|
||
owner.materialPager.replaceChildren();
|
||
const previous = text(owner.materialPager, 'button', t('materialPrevious'), 'anomalous-btn-ghost');
|
||
previous.type = 'button';
|
||
previous.disabled = payload.page <= 1;
|
||
previous.onclick = () => owner.refreshMaterials(payload.page - 1);
|
||
text(owner.materialPager, 'span', t('materialPageSummary', { page: payload.page, pages: payload.pages, count: payload.total }));
|
||
const next = text(owner.materialPager, 'button', t('materialNext'), 'anomalous-btn-ghost');
|
||
next.type = 'button';
|
||
next.disabled = payload.page >= payload.pages;
|
||
next.onclick = () => owner.refreshMaterials(payload.page + 1);
|
||
const tags = owner.materialTagSelect;
|
||
tags.replaceChildren();
|
||
text(tags, 'option', t('materialAllTags')).value = '';
|
||
for (const tag of payload.tags || []) text(tags, 'option', tag).value = tag;
|
||
if (owner.materialTag && !(payload.tags || []).includes(owner.materialTag)) text(tags, 'option', owner.materialTag).value = owner.materialTag;
|
||
tags.value = owner.materialTag || '';
|
||
}
|
||
|
||
export async function refreshMaterials(page = this.materialPage || 1) {
|
||
if (!this.materialList) return;
|
||
this.materialListController?.abort();
|
||
const controller = new AbortController();
|
||
this.materialListController = controller;
|
||
this.materialList.replaceChildren();
|
||
text(this.materialList, 'p', t('loading'), 'anomalous-material-empty');
|
||
const query = new URLSearchParams({ page, limit: 48, q: this.materialQuery || '',
|
||
tag: this.materialTag || '', kind: this.materialKind || '' });
|
||
try {
|
||
const response = await fetch(`/anomalous/materials?${query}`, { cache: 'no-store', signal: controller.signal });
|
||
const payload = await jsonResponse(response, 'material list failed');
|
||
if (this.materialListController !== controller) return;
|
||
this.materialPage = payload.page;
|
||
this.materialList.replaceChildren();
|
||
renderMaterialPagination(this, payload);
|
||
const materials = Array.isArray(payload.materials) ? payload.materials : [];
|
||
if (!materials.length) {
|
||
text(this.materialList, 'p', t(this.materialQuery || this.materialTag || this.materialKind ? 'materialNoMatches' : 'materialEmpty'), 'anomalous-material-empty');
|
||
return;
|
||
}
|
||
const fragment = document.createDocumentFragment();
|
||
for (const material of materials) fragment.appendChild(renderMaterialCard(this, material));
|
||
this.materialList.appendChild(fragment);
|
||
} catch (error) {
|
||
if (error?.name === 'AbortError' || this.materialListController !== controller) return;
|
||
console.error('Could not load materials:', error);
|
||
this.materialList.replaceChildren();
|
||
text(this.materialList, 'p', t('materialLoadError'), 'anomalous-material-empty');
|
||
} finally {
|
||
if (this.materialListController === controller) this.materialListController = null;
|
||
}
|
||
}
|
||
|
||
export async function showMaterials() {
|
||
if (!this.notebookContainer) {
|
||
this.nbPanel.style.display = 'flex';
|
||
await this.showNotebooks();
|
||
}
|
||
hideSiblingWorkspaceViews(this);
|
||
leaveMaterialDetail(this);
|
||
if (!this.materialView) {
|
||
this.materialView = document.createElement('div');
|
||
this.materialView.className = 'anomalous-material-body';
|
||
const intro = document.createElement('div');
|
||
intro.className = 'anomalous-material-intro';
|
||
this.materialIntro = intro;
|
||
const introCopy = document.createElement('div');
|
||
text(introCopy, 'h3', t('materialLibrary'));
|
||
text(introCopy, 'p', t('materialLibraryHint'), 'anomalous-material-muted');
|
||
const refresh = text(intro, 'button', `↻ ${t('refresh')}`, 'anomalous-btn-ghost');
|
||
refresh.type = 'button';
|
||
refresh.onclick = () => this.refreshMaterials();
|
||
intro.prepend(introCopy);
|
||
this.materialView.appendChild(intro);
|
||
buildMaterialFilters(this);
|
||
this.materialList = document.createElement('div');
|
||
this.materialList.className = 'anomalous-material-list';
|
||
this.materialView.appendChild(this.materialList);
|
||
this.materialPager = text(this.materialView, 'nav', '', 'anomalous-material-pager');
|
||
this.materialPager.setAttribute('aria-label', t('materialPagination'));
|
||
this.notebookContainer.appendChild(this.materialView);
|
||
}
|
||
this.materialView.style.display = 'flex';
|
||
await this.refreshMaterials();
|
||
}
|
||
|
||
/**
|
||
* Direct client-side PNG chunk parser to safely extract embedded ComfyUI metadata
|
||
* without requiring a server reboot or server-side re-encoding.
|
||
*/
|
||
export async function showImageMaterialDetail(owner, sourceImage, imageUrl, options = {}) {
|
||
return showImageWorkbench(owner, sourceImage, imageUrl, options);
|
||
}
|
||
|