Make metadata inspector sections collapsible and normalize spacing

This commit is contained in:
Dariusz L
2026-10-16 18:26:07 +02:00
parent 4a5b3ae059
commit 7705cfc688
5 changed files with 571 additions and 29 deletions
+198
View File
@@ -0,0 +1,198 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { imageMetadataMethods } from '../web/resolver/views/tabs_image_metadata_methods.js';
import { tabsLoadedMethods } from '../web/resolver/views/tabs_loaded_methods.js';
globalThis.window = globalThis;
function createMockContext() {
const memory = new Map();
const mockStorage = {
getItem: (k) => (memory.has(k) ? memory.get(k) : null),
setItem: (k, v) => memory.set(k, String(v)),
removeItem: (k) => memory.delete(k),
};
return {
...imageMetadataMethods,
...tabsLoadedMethods,
escapeHtml: (s) => String(s ?? ''),
escapeJsString: (s) => JSON.stringify(s),
getMetadataTooltipBadge: (help, label) => `<span class="mr-tooltip-badge" data-tooltip="${help}" aria-label="${label}">?</span>`,
getCategoryDisplayName: (cat) => cat,
getModelToken: (model) => model.name || '',
getContextMenuAttrs: () => '',
stripModelExtension: (name) => String(name || '').replace(/\.[^/.]+$/, ''),
getMissingModelKey: (model) => model.name || 'key',
getModelPreviewTooltipAttrs: () => '',
bindLoadedModelSelection: () => {},
renderStatusMessage: (msg, type = 'info') => `<div class="mr-status-message ${type}">${msg}</div>`,
storage: mockStorage,
};
}
test('image metadata sections default to expanded', () => {
const ctx = createMockContext();
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('tools'), false);
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('summary'), false);
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), false);
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('model-transfer'), false);
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('models'), false);
});
test('toggling section marks it collapsed and persists state', () => {
const ctx = createMockContext();
const state = {};
ctx.toggleImageInspectorSection('prompt-transfer');
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), true);
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('model-transfer'), false);
ctx.toggleImageInspectorSection('prompt-transfer');
assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), false);
});
test('renderMetadataPromptTransferPanel renders collapse button with chevron and aria-expanded', () => {
const ctx = createMockContext();
const state = {
promptTransfer: {
loading: false,
applying: false,
error: '',
pairs: {},
prompts: { source: [], target: [] },
},
};
const expandedHtml = ctx.renderMetadataPromptTransferPanel(state);
assert.ok(expandedHtml.includes('data-image-inspector-collapse="prompt-transfer"'));
assert.ok(expandedHtml.includes('aria-expanded="true"'));
assert.ok(!expandedHtml.includes('mr-image-prompt-transfer is-collapsed'));
ctx.toggleImageInspectorSection('prompt-transfer');
const collapsedHtml = ctx.renderMetadataPromptTransferPanel(state);
assert.ok(collapsedHtml.includes('aria-expanded="false"'));
assert.ok(collapsedHtml.includes('mr-image-prompt-transfer is-collapsed'));
});
test('renderMetadataTransferPanel renders collapse button with chevron and aria-expanded', () => {
const ctx = createMockContext();
const state = {
transfer: {
loading: false,
refreshing: false,
applying: false,
undoing: false,
error: '',
targetGroups: [],
selectedNodeKeys: new Set(),
},
};
const expandedHtml = ctx.renderMetadataTransferPanel(state);
assert.ok(expandedHtml.includes('data-image-inspector-collapse="model-transfer"'));
assert.ok(expandedHtml.includes('aria-expanded="true"'));
assert.ok(!expandedHtml.includes('mr-image-transfer is-collapsed'));
ctx.toggleImageInspectorSection('model-transfer');
const collapsedHtml = ctx.renderMetadataTransferPanel(state);
assert.ok(collapsedHtml.includes('aria-expanded="false"'));
assert.ok(collapsedHtml.includes('mr-image-transfer is-collapsed'));
});
test('displayLoadedModels renders collapse button and tooltip badge, hiding subtitle when collapsible', () => {
const ctx = createMockContext();
const container = { innerHTML: '', dataset: {} };
const data = {
total: 1,
loaded_models: [
{ name: 'model.safetensors', category: 'checkpoints', active: true }
],
};
// Non-collapsible (Loaded Models tab) - shows subtitle, no tooltip badge
ctx.displayLoadedModels(container, data, { collapsible: false });
assert.ok(!container.innerHTML.includes('data-image-inspector-collapse'));
assert.ok(container.innerHTML.includes('<h3 class="mr-loaded-models-title">Loaded Models'));
assert.ok(container.innerHTML.includes('class="mr-loaded-models-subtitle"'));
// Collapsible (Metadata tab) - shows tooltip badge, hides subtitle paragraph
ctx.displayLoadedModels(container, data, {
collapsible: true,
isCollapsed: false,
sectionId: 'models',
title: 'Referenced Models',
tooltip: 'Models reported by embedded Civitai metadata.',
});
assert.ok(container.innerHTML.includes('data-image-inspector-collapse="models"'));
assert.ok(container.innerHTML.includes('aria-expanded="true"'));
assert.ok(container.innerHTML.includes('Referenced Models'));
assert.ok(container.innerHTML.includes('class="mr-tooltip-badge"'));
assert.ok(container.innerHTML.includes('Models reported by embedded Civitai metadata.'));
assert.ok(!container.innerHTML.includes('class="mr-loaded-models-subtitle"'));
// Collapsible and collapsed
ctx.displayLoadedModels(container, data, {
collapsible: true,
isCollapsed: true,
sectionId: 'models',
title: 'Referenced Models',
tooltip: 'Models reported by embedded Civitai metadata.',
});
assert.ok(container.innerHTML.includes('data-image-inspector-collapse="models"'));
assert.ok(container.innerHTML.includes('aria-expanded="false"'));
});
test('renderImageMetadataResult renders Extracted Metadata with tooltip badge and no subtitle paragraph', () => {
const ctx = createMockContext();
const resultEl = {
innerHTML: '',
querySelector: () => ({ innerHTML: '', dataset: {} }),
};
ctx.contentElement = {
querySelector: (sel) => {
if (sel === '[data-image-inspector-result]') {
return resultEl;
}
return null;
},
};
ctx.imageInspectorState = {
metadata: {
filename: 'test.png',
image: { width: 512, height: 512, format: 'PNG' },
},
loadedModels: null,
loading: false,
error: '',
};
ctx.renderMetadataPromptTransferPanel = () => '';
ctx.renderMetadataTransferPanel = () => '';
ctx.renderImageMetadataResult();
assert.ok(resultEl.innerHTML.includes('Extracted Metadata'));
assert.ok(resultEl.innerHTML.includes('data-image-inspector-collapse="summary"'));
assert.ok(resultEl.innerHTML.includes('Read-only information from the selected source.'));
assert.ok(resultEl.innerHTML.includes('class="mr-tooltip-badge"'));
assert.ok(!resultEl.innerHTML.includes('class="mr-loaded-models-subtitle"'));
});
test('renderImageMetadataResult without metadata renders prompt transfer panel first', () => {
const ctx = createMockContext();
const resultEl = {
innerHTML: '',
querySelector: () => null,
};
ctx.contentElement = {
querySelector: (sel) => (sel === '[data-image-inspector-result]' ? resultEl : null),
};
ctx.imageInspectorState = {
metadata: null,
loadedModels: null,
loading: false,
error: '',
};
ctx.renderMetadataPromptTransferPanel = () => '<section class="mr-image-prompt-transfer is-collapsed"></section>';
ctx.renderMetadataTransferPanel = () => '<section class="mr-image-transfer is-collapsed"></section>';
ctx.renderImageMetadataResult();
assert.ok(resultEl.innerHTML.startsWith('<section class="mr-image-prompt-transfer'));
assert.ok(!resultEl.innerHTML.includes('Extracted Metadata'));
});
+169 -6
View File
@@ -4308,6 +4308,74 @@
color: var(--mr-text-muted);
}
#model-resolver-modal .mr-section-collapse-header-row,
.mr-section-collapse-header-row {
display: flex;
align-items: center;
gap: var(--mr-spacing-6);
min-width: 0;
}
#model-resolver-modal .mr-section-collapse-btn,
.mr-section-collapse-btn {
display: inline-flex;
align-items: center;
gap: var(--mr-spacing-6);
background: transparent;
border: 0;
padding: 0;
margin: 0;
cursor: pointer;
font: inherit;
color: inherit;
text-align: left;
box-shadow: none;
}
#model-resolver-modal .mr-section-collapse-btn:hover .mr-loaded-models-title,
.mr-section-collapse-btn:hover .mr-loaded-models-title {
color: #ffffff;
}
#model-resolver-modal .mr-section-collapse-btn:hover .mr-section-collapse-chevron,
.mr-section-collapse-btn:hover .mr-section-collapse-chevron {
color: #ffffff;
}
#model-resolver-modal .mr-section-collapse-btn:focus-visible,
.mr-section-collapse-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--mr-missing-control-focus-ring, rgba(114, 211, 138, 0.4));
border-radius: var(--mr-radius-sm, 4px);
}
#model-resolver-modal .mr-section-collapse-chevron,
.mr-section-collapse-chevron {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 18px;
width: 18px;
height: 18px;
border: 0;
border-radius: 0;
color: var(--mr-text-secondary);
background: transparent;
box-shadow: none;
transition: transform 140ms ease, color 140ms ease;
}
#model-resolver-modal .mr-section-collapse-chevron svg,
.mr-section-collapse-chevron svg {
width: 16px;
height: 16px;
}
#model-resolver-modal .mr-section-collapse-btn[aria-expanded="false"] .mr-section-collapse-chevron,
.mr-section-collapse-btn[aria-expanded="false"] .mr-section-collapse-chevron {
transform: rotate(-90deg);
}
.mr-models-list {
padding: var(--mr-spacing-8) var(--mr-spacing-12) var(--mr-spacing-10);
display: flex;
@@ -4937,6 +5005,15 @@ button.mr-info-link:disabled {
padding: 0 var(--mr-spacing-12) var(--mr-spacing-16);
}
#model-resolver-modal .mr-image-inspector-result > :first-child,
.mr-image-inspector-result > :first-child,
#model-resolver-modal .mr-image-inspector-result > .mr-image-prompt-transfer:first-child,
.mr-image-inspector-result > .mr-image-prompt-transfer:first-child,
#model-resolver-modal .mr-image-inspector-result > .mr-image-transfer:first-child,
.mr-image-inspector-result > .mr-image-transfer:first-child {
margin-top: 0;
}
.mr-image-inspector-summary {
padding-top: var(--mr-spacing-12);
border-top: 1px solid var(--mr-border);
@@ -4944,9 +5021,7 @@ button.mr-info-link:disabled {
#model-resolver-modal .mr-image-inspector-summary .mr-loaded-models-header,
.mr-image-inspector-summary .mr-loaded-models-header {
padding-left: 0;
padding-right: 0;
padding-top: 0;
padding: 0;
border-bottom: 0;
margin-bottom: var(--mr-spacing-8);
}
@@ -4987,9 +5062,9 @@ button.mr-info-link:disabled {
#model-resolver-modal .mr-image-inspector-models .mr-loaded-models-header,
.mr-image-inspector-models .mr-loaded-models-header {
padding-left: 0;
padding-right: 0;
padding-top: 0;
padding: 0;
border-bottom: 0;
align-items: flex-start;
}
#model-resolver-modal .mr-image-inspector-models .mr-models-list,
@@ -5064,6 +5139,94 @@ button.mr-info-link:disabled {
background: linear-gradient(90deg, rgba(114, 211, 138, 0.28), var(--mr-border) 34%, transparent 100%);
}
#model-resolver-modal .mr-image-inspector.is-tools-collapsed .mr-image-inspector-tools,
.mr-image-inspector.is-tools-collapsed .mr-image-inspector-tools {
display: none;
}
#model-resolver-modal .mr-image-inspector.is-tools-collapsed .mr-image-inspector-header,
.mr-image-inspector.is-tools-collapsed .mr-image-inspector-header {
padding-bottom: var(--mr-spacing-12);
}
#model-resolver-modal .mr-image-inspector-summary.is-collapsed .mr-image-inspector-info-table,
.mr-image-inspector-summary.is-collapsed .mr-image-inspector-info-table {
display: none;
}
#model-resolver-modal .mr-image-inspector-summary.is-collapsed .mr-loaded-models-header,
.mr-image-inspector-summary.is-collapsed .mr-loaded-models-header {
margin-bottom: 0;
padding: 0;
}
#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-list,
.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-list,
#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-actions,
.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-actions,
#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-error-text,
.mr-image-prompt-transfer.is-collapsed .mr-error-text {
display: none;
}
#model-resolver-modal .mr-image-prompt-transfer.is-collapsed,
.mr-image-prompt-transfer.is-collapsed {
margin-bottom: var(--mr-spacing-12);
}
#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-header,
.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-header {
margin-bottom: 0;
}
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-mode,
.mr-image-transfer.is-collapsed .mr-image-transfer-mode,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-activity,
.mr-image-transfer.is-collapsed .mr-image-transfer-activity,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-category-list,
.mr-image-transfer.is-collapsed .mr-image-transfer-category-list,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-actions,
.mr-image-transfer.is-collapsed .mr-image-transfer-actions,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-loading,
.mr-image-transfer.is-collapsed .mr-image-transfer-loading,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-note,
.mr-image-transfer.is-collapsed .mr-image-transfer-note,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-inspector-loading,
.mr-image-transfer.is-collapsed .mr-image-inspector-loading,
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-error-text,
.mr-image-transfer.is-collapsed .mr-error-text {
display: none;
}
#model-resolver-modal .mr-image-transfer.is-collapsed,
.mr-image-transfer.is-collapsed {
margin-bottom: var(--mr-spacing-12);
}
#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-header,
.mr-image-transfer.is-collapsed .mr-image-transfer-header {
margin-bottom: 0;
}
#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-models-list,
.mr-image-inspector-models.is-collapsed .mr-models-list,
#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-models-list-pad,
.mr-image-inspector-models.is-collapsed .mr-models-list-pad,
#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-copy-section,
.mr-image-inspector-models.is-collapsed .mr-copy-section,
#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-loaded-filter-row,
.mr-image-inspector-models.is-collapsed .mr-loaded-filter-row {
display: none;
}
#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-loaded-models-header,
.mr-image-inspector-models.is-collapsed .mr-loaded-models-header {
margin-bottom: 0;
padding: 0;
border-bottom: 0;
align-items: flex-start;
}
.mr-image-prompt-header,
.mr-image-prompt-actions {
display: flex;
+157 -19
View File
@@ -1,4 +1,5 @@
import { copyTextWithFeedback, safeStorage } from "../utils/html_utils.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import {
isExistingResolvedModel,
toResolverContextModel,
@@ -15,6 +16,7 @@ const MAX_IMAGE_FILE_SIZE = 64 * 1024 * 1024;
const MAX_WORKFLOW_FILE_SIZE = 16 * 1024 * 1024;
const METADATA_TRANSFER_MODE_STORAGE_KEY = 'model_resolver_metadata_transfer_mode';
const METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY = 'model_resolver_metadata_transfer_activity_scope';
const METADATA_COLLAPSED_SECTIONS_STORAGE_KEY = 'model_resolver_metadata_collapsed_sections';
const METADATA_PROMPT_ROLES = ['positive', 'negative'];
const MAX_PROMPT_PREVIEW_LENGTH = 12_000;
const COPY_ICON_HTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
@@ -84,6 +86,7 @@ function getImageInspectorState(dialog) {
transfer: null,
promptTransfer: null,
lastTransfer: null,
collapsedSections: null,
};
}
return dialog.imageInspectorState;
@@ -184,6 +187,84 @@ export const imageMetadataMethods = {
return Boolean(workflowSignature) && transfer.workflowSignature === workflowSignature;
},
getImageInspectorCollapsedSections(state = null) {
const rootState = getImageInspectorState(this);
const imageState = state || rootState;
if (rootState && rootState.collapsedSections instanceof Set) {
imageState.collapsedSections = rootState.collapsedSections;
return rootState.collapsedSections;
}
if (!(imageState.collapsedSections instanceof Set)) {
let initial = [];
const stored = safeStorage.getItem(METADATA_COLLAPSED_SECTIONS_STORAGE_KEY);
if (stored) {
try {
initial = JSON.parse(stored);
} catch {
initial = [];
}
}
const set = new Set(Array.isArray(initial) ? initial : []);
imageState.collapsedSections = set;
if (rootState) rootState.collapsedSections = set;
}
return imageState.collapsedSections;
},
isImageInspectorSectionCollapsed(sectionId, state = null) {
if (!sectionId) return false;
const collapsed = this.getImageInspectorCollapsedSections(state);
return collapsed instanceof Set ? collapsed.has(sectionId) : false;
},
toggleImageInspectorSection(sectionId) {
if (!sectionId) return;
const state = getImageInspectorState(this);
const collapsed = this.getImageInspectorCollapsedSections(state);
const nextCollapsed = !collapsed.has(sectionId);
if (nextCollapsed) {
collapsed.add(sectionId);
} else {
collapsed.delete(sectionId);
}
try {
safeStorage.setItem(
METADATA_COLLAPSED_SECTIONS_STORAGE_KEY,
JSON.stringify(Array.from(collapsed)),
);
} catch {
// ignore storage errors
}
this.applyImageInspectorSectionCollapsed(sectionId, nextCollapsed);
},
applyImageInspectorSectionCollapsed(sectionId, isCollapsed) {
const root = this.contentElement?.querySelector('.mr-image-inspector');
if (!root) return;
let container = null;
if (sectionId === 'tools') {
root.classList.toggle('is-tools-collapsed', isCollapsed);
} else if (sectionId === 'summary') {
container = root.querySelector('.mr-image-inspector-summary');
} else if (sectionId === 'prompt-transfer') {
container = root.querySelector('.mr-image-prompt-transfer');
} else if (sectionId === 'model-transfer') {
container = root.querySelector('.mr-image-transfer');
} else if (sectionId === 'models') {
container = root.querySelector('.mr-image-inspector-models');
}
if (container) {
container.classList.toggle('is-collapsed', isCollapsed);
}
const btn = root.querySelector(`[data-image-inspector-collapse="${sectionId}"]`);
if (btn) {
btn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
}
},
loadImageMetadata() {
if (!this.contentElement) return null;
this.contentElement.style.overflowY = 'auto';
@@ -198,13 +279,20 @@ export const imageMetadataMethods = {
renderImageMetadataShell() {
if (!this.contentElement) return;
const isToolsCollapsed = this.isImageInspectorSectionCollapsed('tools');
const existingRoot = this.contentElement.querySelector('.mr-image-inspector');
if (!existingRoot) {
this.contentElement.innerHTML = `
<div class="mr-image-inspector">
<div class="mr-image-inspector${isToolsCollapsed ? ' is-tools-collapsed' : ''}" data-inspector-section="tools">
<div class="mr-loaded-models-header mr-image-inspector-header">
<div class="mr-loaded-title-block">
<h3 class="mr-loaded-models-title">Image Metadata ${this.getMetadataTooltipBadge('Drop an image or workflow JSON here, or paste JSON into the box below.', 'Image metadata help')}</h3>
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="tools" aria-expanded="${isToolsCollapsed ? 'false' : 'true'}" aria-label="Toggle Image Metadata section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">Image Metadata</span>
</button>
${this.getMetadataTooltipBadge('Drop an image or workflow JSON here, or paste JSON into the box below.', 'Image metadata help')}
</div>
</div>
</div>
<div class="mr-image-inspector-tools">
@@ -222,6 +310,10 @@ export const imageMetadataMethods = {
</div>
`;
this.bindImageMetadataEvents();
} else {
existingRoot.classList.toggle('is-tools-collapsed', isToolsCollapsed);
const btn = existingRoot.querySelector('[data-image-inspector-collapse="tools"]');
if (btn) btn.setAttribute('aria-expanded', isToolsCollapsed ? 'false' : 'true');
}
this.bindTooltips?.(this.contentElement);
@@ -251,6 +343,14 @@ export const imageMetadataMethods = {
let dragDepth = 0;
root.addEventListener('click', (event) => {
const collapseToggle = event.target.closest?.('[data-image-inspector-collapse]');
if (collapseToggle) {
event.preventDefault();
event.stopPropagation();
this.toggleImageInspectorSection(collapseToggle.dataset.imageInspectorCollapse);
return;
}
const categoryToggle = event.target.closest?.('[data-image-transfer-category-toggle]');
if (categoryToggle) {
this.toggleMetadataTransferCategory(categoryToggle.dataset.imageTransferCategoryToggle);
@@ -632,20 +732,27 @@ export const imageMetadataMethods = {
</tr>
` : '';
const modelContainerId = `mr-image-inspector-models-${Date.now()}`;
const isSummaryCollapsed = this.isImageInspectorSectionCollapsed('summary', state);
const isModelsCollapsed = this.isImageInspectorSectionCollapsed('models', state);
result.innerHTML = `
<section class="mr-image-inspector-summary">
<section class="mr-image-inspector-summary${isSummaryCollapsed ? ' is-collapsed' : ''}" data-inspector-section="summary">
<div class="mr-loaded-models-header">
<div class="mr-loaded-title-block">
<h3 class="mr-loaded-models-title">Extracted Metadata</h3>
<p class="mr-loaded-models-subtitle">Read-only information from the selected source.</p>
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="summary" aria-expanded="${isSummaryCollapsed ? 'false' : 'true'}" aria-label="Toggle Extracted Metadata section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">Extracted Metadata</span>
</button>
${this.getMetadataTooltipBadge('Read-only information from the selected source.', 'Extracted metadata help')}
</div>
</div>
</div>
<table class="mr-info-table mr-image-inspector-info-table"><tbody>${rowsHtml}${parametersHtml}</tbody></table>
</section>
${this.renderMetadataPromptTransferPanel(state)}
${this.renderMetadataTransferPanel(state)}
<section class="mr-image-inspector-models">
<section class="mr-image-inspector-models${isModelsCollapsed ? ' is-collapsed' : ''}" data-inspector-section="models">
<div id="${this.escapeHtml(modelContainerId)}"></div>
</section>
`;
@@ -654,8 +761,11 @@ export const imageMetadataMethods = {
if (!metadata.workflow && civitaiModelData.total) {
this.displayLoadedModels(modelContainer, civitaiModelData, {
title: 'Referenced Models',
subtitle: 'Models reported by embedded Civitai metadata.',
tooltip: 'Models reported by embedded Civitai metadata.',
includeContextMenu: false,
collapsible: true,
isCollapsed: isModelsCollapsed,
sectionId: 'models',
});
} else if (!metadata.workflow) {
modelContainer.innerHTML = this.renderStatusMessage(
@@ -663,7 +773,13 @@ export const imageMetadataMethods = {
'info'
);
} else if (state.loadedModels) {
this.displayLoadedModels(modelContainer, state.loadedModels);
this.displayLoadedModels(modelContainer, state.loadedModels, {
title: 'Referenced Models',
tooltip: 'Models detected in the imported ComfyUI workflow.',
collapsible: true,
isCollapsed: isModelsCollapsed,
sectionId: 'models',
});
} else {
modelContainer.innerHTML = this.renderStatusMessage(
'No loaded model scan is available yet.',
@@ -953,12 +1069,17 @@ export const imageMetadataMethods = {
const statusHtml = promptTransfer.error
? `<p class="mr-error-text">${this.escapeHtml(promptTransfer.error)}</p>`
: '';
const isPromptCollapsed = this.isImageInspectorSectionCollapsed?.('prompt-transfer', imageState) || false;
return `
<section class="mr-image-prompt-transfer">
<section class="mr-image-prompt-transfer${isPromptCollapsed ? ' is-collapsed' : ''}" data-inspector-section="prompt-transfer">
<div class="mr-image-prompt-header">
<div>
<h3 class="mr-loaded-models-title">Transfer prompts to current workflow ${this.getMetadataTooltipBadge(promptHelpText, 'Prompt transfer help')}</h3>
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="prompt-transfer" aria-expanded="${isPromptCollapsed ? 'false' : 'true'}" aria-label="Toggle prompt transfer section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">Transfer prompts to current workflow</span>
</button>
${this.getMetadataTooltipBadge(promptHelpText, 'Prompt transfer help')}
</div>
</div>
${statusHtml}
@@ -1717,9 +1838,19 @@ export const imageMetadataMethods = {
? 'The imported image has no embedded ComfyUI workflow.'
: 'Import a workflow with model metadata to enable Apply transfer.';
const isTransferCollapsed = this.isImageInspectorSectionCollapsed?.('model-transfer', state) || false;
if (transfer.loading) {
return `
<section class="mr-image-transfer mr-image-transfer-loading">
<section class="mr-image-transfer mr-image-transfer-loading${isTransferCollapsed ? ' is-collapsed' : ''}" data-inspector-section="model-transfer">
<div class="mr-image-transfer-header">
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="model-transfer" aria-expanded="${isTransferCollapsed ? 'false' : 'true'}" aria-label="Toggle model transfer section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">Transfer models to current workflow</span>
</button>
</div>
</div>
<div class="mr-image-inspector-loading">Scanning the current workflow for target nodes...</div>
<p class="mr-image-transfer-note">${this.escapeHtml(sourceSummary)}</p>
</section>
@@ -1727,17 +1858,20 @@ export const imageMetadataMethods = {
}
if (transfer.error) {
return `
<section class="mr-image-transfer">
<section class="mr-image-transfer${isTransferCollapsed ? ' is-collapsed' : ''}" data-inspector-section="model-transfer">
<div class="mr-image-transfer-header">
<div>
<h3 class="mr-loaded-models-title">Transfer models to current workflow</h3>
<p class="mr-error-text">${this.escapeHtml(transfer.error)}</p>
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="model-transfer" aria-expanded="${isTransferCollapsed ? 'false' : 'true'}" aria-label="Toggle model transfer section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">Transfer models to current workflow</span>
</button>
</div>
<div class="mr-image-inspector-header-actions">
${undoButton}
<button type="button" class="mr-btn mr-btn-secondary mr-btn-sm" data-image-inspector-action="retry-transfer-targets">Retry</button>
</div>
</div>
<p class="mr-error-text">${this.escapeHtml(transfer.error)}</p>
<p class="mr-image-transfer-note">${this.escapeHtml(sourceSummary)}</p>
</section>
`;
@@ -1907,10 +2041,14 @@ export const imageMetadataMethods = {
: '',
].filter(Boolean).join('\n\n');
return `
<section class="mr-image-transfer">
<section class="mr-image-transfer${isTransferCollapsed ? ' is-collapsed' : ''}" data-inspector-section="model-transfer">
<div class="mr-image-transfer-header">
<div>
<h3 class="mr-loaded-models-title">Transfer models to current workflow ${this.getMetadataTooltipBadge(transferHelpText, 'Model transfer help')}</h3>
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="model-transfer" aria-expanded="${isTransferCollapsed ? 'false' : 'true'}" aria-label="Toggle model transfer section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">Transfer models to current workflow</span>
</button>
${this.getMetadataTooltipBadge(transferHelpText, 'Model transfer help')}
</div>
<div class="mr-image-inspector-header-actions">
${undoButton}
+28 -2
View File
@@ -397,11 +397,36 @@ export const tabsLoadedMethods = {
: '';
const modelSubtitle = subtitle || `${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}`;
const isCollapsed = Boolean(options.isCollapsed);
const tooltipText = options.tooltip || (options.collapsible && subtitle ? subtitle : '');
const tooltipHtml = tooltipText
? (typeof this.getMetadataTooltipBadge === 'function'
? this.getMetadataTooltipBadge(tooltipText, `${title} help`)
: `<span class="mr-tooltip-badge" data-tooltip="${this.escapeHtml(tooltipText)}" aria-label="${this.escapeHtml(title)} help">?</span>`)
: '';
const titleHtml = options.collapsible
? `
<div class="mr-section-collapse-header-row">
<button type="button" class="mr-section-collapse-btn" data-image-inspector-collapse="${this.escapeHtml(options.sectionId || 'models')}" aria-expanded="${isCollapsed ? 'false' : 'true'}" aria-label="Toggle ${this.escapeHtml(title)} section">
<span class="mr-section-collapse-chevron" aria-hidden="true">${getSvgIcon('chevronDown')}</span>
<span class="mr-loaded-models-title">${this.escapeHtml(title)} <span class="mr-loaded-total">${total}</span></span>
</button>
${tooltipHtml}
</div>
`
: `<h3 class="mr-loaded-models-title">${this.escapeHtml(title)} <span class="mr-loaded-total">${total}</span>${tooltipHtml}</h3>`;
const showSubtitle = (options.showSubtitle ?? !options.collapsible) && Boolean(modelSubtitle);
const subtitleHtml = showSubtitle
? `<p class="mr-loaded-models-subtitle">${this.escapeHtml(modelSubtitle)}</p>`
: '';
let html = `
<div class="mr-loaded-models-header">
<div class="mr-loaded-title-block">
<h3 class="mr-loaded-models-title">${this.escapeHtml(title)} <span class="mr-loaded-total">${total}</span></h3>
<p class="mr-loaded-models-subtitle">${this.escapeHtml(modelSubtitle)}</p>
${titleHtml}
${subtitleHtml}
</div>
<div class="mr-loaded-filter-row">
<button class="mr-btn-filter active" id="filter-all" onclick="window.MLFilterSwitch('all', this)">All (${activeCount + inactiveCount})</button>
@@ -504,6 +529,7 @@ export const tabsLoadedMethods = {
if (typeof container?.querySelector === 'function') {
this.bindLoadedModelSelection?.(container);
this.bindTooltips?.(container);
}
},
+19 -2
View File
@@ -1,5 +1,19 @@
// icon_utils - Utility for loading SVG icons
import { app } from "../../../scripts/app.js";
let appInstance = null;
async function getApp() {
if (appInstance) return appInstance;
if (typeof window !== "undefined" && window.app) {
appInstance = window.app;
return appInstance;
}
try {
const mod = await import("../../../scripts/app.js");
appInstance = mod?.app || null;
} catch {
// Not in ComfyUI environment
}
return appInstance;
}
const SVG_ICONS = {
trash2: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M5 6l1 14a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1l1-14M10 10v7M14 10v7"></path></svg>`,
@@ -100,7 +114,10 @@ export function loadIcons(icons = {}, iconColor = "#dddddd", userColors = {}) {
const svg = SVG_ICONS[name].replaceAll("{color}", color);
const img = new Image();
img.onload = () => app.graph.setDirtyCanvas(true);
img.onload = async () => {
const app = await getApp();
app?.graph?.setDirtyCanvas(true);
};
img.src = `data:image/svg+xml;base64,${btoa(svg)}`;
icons[name] = img;