Make metadata inspector sections collapsible and normalize spacing
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user