Store references to queue title, toggle, and clear buttons as instance properties in the constructor, replacing repeated DOM queries inside updateQueueUI with direct reference lookups.
1019 lines
47 KiB
JavaScript
1019 lines
47 KiB
JavaScript
import { app } from "../../../../../scripts/app.js";
|
|
import { api } from "../../../../../scripts/api.js";
|
|
import { $el } from "../../../../../scripts/ui.js";
|
|
import { getSvgIcon } from "../../utils/icon_utils.js";
|
|
export const queueMethods = {
|
|
createContent() {
|
|
// Wrap the body in a two-column layout: left = items, right = queued panel
|
|
const body = $el("div", {
|
|
id: "model-resolver-body"
|
|
});
|
|
|
|
this.contentElement = $el("div.mr-scrollable", {
|
|
id: "model-resolver-content"
|
|
});
|
|
|
|
this.queueElement = $el("div", {
|
|
id: "model-resolver-queue"
|
|
}, [
|
|
this.createQueuePanel()
|
|
]);
|
|
|
|
// Splitter between content and queue
|
|
this.splitterElement = $el("div", {
|
|
id: "model-resolver-splitter",
|
|
ondragstart: (e) => e.preventDefault()
|
|
});
|
|
|
|
body.appendChild(this.contentElement);
|
|
body.appendChild(this.splitterElement);
|
|
body.appendChild(this.queueElement);
|
|
|
|
// Restore saved queue width and wire splitter
|
|
try {
|
|
const savedSplit = localStorage.getItem('model_resolver_split_w');
|
|
if (savedSplit) {
|
|
const w = parseInt(savedSplit, 10);
|
|
if (!isNaN(w) && w > 0) {
|
|
this.queueElement.style.width = `${w}px`;
|
|
}
|
|
}
|
|
} catch (e) { }
|
|
|
|
try {
|
|
const onSplitMouseDown = (e) => this.startSplitDrag(e);
|
|
this.splitterElement.addEventListener('mousedown', onSplitMouseDown);
|
|
this._splitterMouseDown = onSplitMouseDown;
|
|
} catch (e) { }
|
|
|
|
// Toggle icon always visible
|
|
try {
|
|
this.queueToggleIcon = $el("button", {
|
|
id: "queue-toggle-icon",
|
|
type: "button",
|
|
"aria-label": "Toggle queued selections",
|
|
onclick: (event) => this.onQueueEdgeClick(event),
|
|
onmousedown: (event) => this.startQueueEdgeDrag(event)
|
|
});
|
|
body.appendChild(this.queueToggleIcon);
|
|
this.updateQueueToggleIcon();
|
|
} catch (e) { }
|
|
|
|
// Restore queue collapsed state
|
|
try {
|
|
const col = localStorage.getItem('model_resolver_queue_collapsed');
|
|
if (col === '1') this.setQueueCollapsed(true);
|
|
} catch (e) { }
|
|
|
|
return body;
|
|
},
|
|
|
|
createQueuePanel() {
|
|
this.queueTitle = $el("div#queue-title.mr-queue-title", { textContent: "Queued Selections (0)" });
|
|
this.queueToggleButton = $el("button", {
|
|
id: "queue-toggle",
|
|
className: "mr-btn mr-btn-secondary mr-btn-sm",
|
|
textContent: "Collapse",
|
|
onclick: () => this.toggleQueueCollapsed()
|
|
});
|
|
this.queueClearButton = $el("button", {
|
|
id: "queue-clear",
|
|
className: "mr-btn mr-btn-secondary mr-btn-sm",
|
|
textContent: "Clear All",
|
|
onclick: () => this.clearAllQueued()
|
|
});
|
|
|
|
// Header row with title and clear button
|
|
this.queueHeader = $el("div.mr-queue-header", {}, [
|
|
this.queueTitle,
|
|
$el("div.mr-queue-actions", {}, [
|
|
this.queueToggleButton,
|
|
this.queueClearButton
|
|
])
|
|
]);
|
|
|
|
// Scrollable list
|
|
this.queueList = $el("div#queue-list.mr-queue-list");
|
|
|
|
const panel = $el("div.mr-queue-panel", {}, [
|
|
$el("div.mr-queue-stack", {}, [this.queueHeader, this.queueList])
|
|
]);
|
|
return panel;
|
|
},
|
|
|
|
updateQueuePanel() {
|
|
if (!this.queueList || !this.queueHeader) return;
|
|
const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : [];
|
|
// Update title count
|
|
const title = this.queueTitle || this.queueHeader.querySelector('#queue-title');
|
|
if (title) title.textContent = `Queued Selections (${list.length})`;
|
|
const toggleBtn = this.queueToggleButton || this.queueHeader.querySelector('#queue-toggle');
|
|
if (toggleBtn) toggleBtn.textContent = this.queueCollapsed ? 'Expand' : 'Collapse';
|
|
|
|
if (!list.length) {
|
|
this.queueList.innerHTML = '<div class="mr-queue-empty">No selections queued.</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="mr-queue-items">';
|
|
for (let i = 0; i < list.length; i++) {
|
|
const r = list[i];
|
|
const label = (r.resolved_model?.relative_path || r.resolved_model?.filename || r.resolved_path || '').toString();
|
|
const nodeLabel = r.node_label || r.node_type || (r.subgraph_id ? 'Subgraph' : 'Node');
|
|
const orig = (r.original_path || '').toString();
|
|
const rmId = `queue-remove-${i}`;
|
|
html += `<div class="mr-queue-item">`;
|
|
html += `<div class="mr-queue-item-title">${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(r.node_id))}</div>`;
|
|
html += `<div class="mr-queue-item-meta"><span>Original</span><code>${this.escapeHtml(orig)}</code></div>`;
|
|
html += `<div class="mr-queue-item-selection"><span>Selected</span><code>${this.escapeHtml(label)}</code></div>`;
|
|
html += `<div class="mr-queue-item-actions"><button id="${rmId}" class="mr-btn mr-btn-secondary mr-btn-sm">Remove</button></div>`;
|
|
html += `</div>`;
|
|
}
|
|
html += '</div>';
|
|
this.queueList.innerHTML = html;
|
|
|
|
// Wire remove buttons
|
|
for (let i = 0; i < list.length; i++) {
|
|
const rmId = `queue-remove-${i}`;
|
|
const btn = this.queueList.querySelector(`#${rmId}`);
|
|
if (btn) {
|
|
btn.addEventListener('click', () => this.removeQueuedByIndex(i));
|
|
}
|
|
}
|
|
},
|
|
|
|
// Remove queued by index
|
|
removeQueuedByIndex(i) {
|
|
const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : [];
|
|
if (i < 0 || i >= list.length) return;
|
|
const r = list[i];
|
|
// Remove
|
|
this.pendingResolutions.splice(i, 1);
|
|
this.rebuildPendingIndex();
|
|
this.savePendingQueueForActiveWorkflow();
|
|
// Update per-item selected bar
|
|
const m = { node_id: r.node_id, widget_index: r.widget_index, subgraph_id: r.subgraph_id, is_top_level: r.is_top_level };
|
|
this.updateSelectedBarForMissing?.(m);
|
|
this.updateApplyPendingButton?.();
|
|
this.updateQueuePanel();
|
|
},
|
|
|
|
// Clear all queued selections
|
|
clearAllQueued() {
|
|
this.pendingResolutions = [];
|
|
this.pendingIndex = new Map();
|
|
this.savePendingQueueForActiveWorkflow();
|
|
this.updateApplyPendingButton?.();
|
|
this.updateQueuePanel();
|
|
try {
|
|
document.querySelectorAll('.model-resolver-selected').forEach(el => { el.style.display = 'none'; el.innerHTML = ''; });
|
|
} catch (e) { /* ignore */ }
|
|
},
|
|
|
|
// Update selected bar for a specific missing model slot
|
|
updateSelectedBarForMissing(missing) {
|
|
if (!missing) return;
|
|
const nodeId = missing.node_id;
|
|
const widgetIndex = missing.widget_index;
|
|
const subgraphId = missing.subgraph_id || '';
|
|
const isTopLevel = missing.is_top_level !== false;
|
|
const key = `${nodeId}:${widgetIndex}:${subgraphId}:${isTopLevel ? 'T' : 'F'}`;
|
|
|
|
const selectedBar = document.getElementById(`selected-bar-${nodeId}-${widgetIndex}`);
|
|
if (!selectedBar) return;
|
|
|
|
// Find selection for this slot
|
|
let selection = null;
|
|
let selectionIdx = -1;
|
|
if (this.pendingIndex.has(key)) {
|
|
const idx = this.pendingIndex.get(key);
|
|
if (idx >= 0 && idx < this.pendingResolutions.length) {
|
|
selection = this.pendingResolutions[idx];
|
|
selectionIdx = idx;
|
|
}
|
|
}
|
|
|
|
if (!selection) {
|
|
selectedBar.style.display = 'none';
|
|
selectedBar.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
// Build selected bar content
|
|
const label = selection.resolved_model?.relative_path || selection.resolved_model?.filename || selection.resolved_path || '';
|
|
const resolveBtnId = `selected-remove-${nodeId}-${widgetIndex}`;
|
|
|
|
selectedBar.innerHTML = `<div class="mr-selected-bar-inner">`;
|
|
selectedBar.innerHTML += `<span class="mr-selected-label">✓ Selected:</span>`;
|
|
selectedBar.innerHTML += `<code class="mr-selected-code">${label}</code>`;
|
|
selectedBar.innerHTML += `<button id="${resolveBtnId}" class="mr-btn mr-btn-secondary mr-btn-sm">Remove</button>`;
|
|
selectedBar.innerHTML += `</div>`;
|
|
selectedBar.style.display = 'block';
|
|
|
|
// Wire remove button - use key-based removal
|
|
const removeBtn = selectedBar.querySelector(`#${resolveBtnId}`);
|
|
if (removeBtn) {
|
|
removeBtn.addEventListener('click', () => {
|
|
this.removeQueuedByKey(key);
|
|
});
|
|
}
|
|
},
|
|
|
|
// Remove queued by key (more reliable than index)
|
|
removeQueuedByKey(key) {
|
|
if (!key || !this.pendingIndex.has(key)) return;
|
|
|
|
// Find and remove the item with this key
|
|
const idx = this.pendingIndex.get(key);
|
|
if (idx >= 0 && idx < this.pendingResolutions.length) {
|
|
const r = this.pendingResolutions[idx];
|
|
// Update the selected bar before removing
|
|
const m = { node_id: r.node_id, widget_index: r.widget_index, subgraph_id: r.subgraph_id, is_top_level: r.is_top_level };
|
|
|
|
// Remove from array
|
|
this.pendingResolutions.splice(idx, 1);
|
|
this.rebuildPendingIndex();
|
|
this.savePendingQueueForActiveWorkflow();
|
|
this.updateSelectedBarForMissing(m);
|
|
this.updateApplyPendingButton?.();
|
|
this.updateQueuePanel();
|
|
}
|
|
},
|
|
|
|
// Rebuild pending index after modification
|
|
rebuildPendingIndex() {
|
|
this.pendingIndex = new Map();
|
|
for (let i = 0; i < this.pendingResolutions.length; i++) {
|
|
const r = this.pendingResolutions[i];
|
|
const key = `${r.node_id}:${r.widget_index}:${r.subgraph_id || ''}:${r.is_top_level ? 'T' : 'F'}`;
|
|
this.pendingIndex.set(key, i);
|
|
}
|
|
},
|
|
|
|
// Collapse/expand queue panel
|
|
toggleQueueCollapsed() {
|
|
this.setQueueCollapsed(!this.queueCollapsed);
|
|
},
|
|
|
|
setQueueCollapsed(collapsed) {
|
|
this.queueCollapsed = !!collapsed;
|
|
if (this.queueCollapsed) {
|
|
try { localStorage.setItem('model_resolver_queue_collapsed', '1'); } catch (e) { }
|
|
} else {
|
|
try { localStorage.setItem('model_resolver_queue_collapsed', '0'); } catch (e) { }
|
|
}
|
|
this.updateQueueVisibility();
|
|
this.updateQueuePanel();
|
|
},
|
|
|
|
updateQueueVisibility() {
|
|
const isMissingTab = this.activeTab === 'missing';
|
|
const showQueuePanel = isMissingTab && !this.queueCollapsed;
|
|
|
|
if (this.queueElement) {
|
|
this.queueElement.style.display = showQueuePanel ? '' : 'none';
|
|
}
|
|
if (this.splitterElement) {
|
|
this.splitterElement.style.display = showQueuePanel ? '' : 'none';
|
|
}
|
|
if (this.queueToggleIcon) {
|
|
this.queueToggleIcon.style.display = isMissingTab ? '' : 'none';
|
|
}
|
|
|
|
this.updateQueueToggleIcon();
|
|
},
|
|
|
|
updateQueueToggleIcon() {
|
|
if (!this.queueToggleIcon) return;
|
|
this.queueToggleIcon.style.display = this.activeTab === 'missing' ? '' : 'none';
|
|
this.queueToggleIcon.textContent = '';
|
|
this.queueToggleIcon.removeAttribute('data-tooltip');
|
|
this.queueToggleIcon.removeAttribute('title');
|
|
if (this.queueCollapsed) {
|
|
this.queueToggleIcon.classList.add('is-collapsed');
|
|
this.queueToggleIcon.setAttribute('aria-label', 'Show queued selections');
|
|
} else {
|
|
this.queueToggleIcon.classList.remove('is-collapsed');
|
|
this.queueToggleIcon.setAttribute('aria-label', 'Hide queued selections');
|
|
}
|
|
},
|
|
|
|
onQueueEdgeClick(event) {
|
|
if (this._suppressQueueEdgeClick) {
|
|
event?.preventDefault?.();
|
|
event?.stopPropagation?.();
|
|
this._suppressQueueEdgeClick = false;
|
|
return;
|
|
}
|
|
this.toggleQueueCollapsed();
|
|
},
|
|
|
|
startQueueEdgeDrag(event) {
|
|
if (event.button !== 0 || !this.queueCollapsed || !this.queueElement) return;
|
|
|
|
const startX = event.clientX;
|
|
const previousCursor = document.body.style.cursor;
|
|
event.preventDefault();
|
|
document.body.style.cursor = 'col-resize';
|
|
this._suppressQueueEdgeClick = true;
|
|
this.splitterElement.style.display = '';
|
|
document.getElementById('model-resolver-body')?.classList.add('mr-queue-preview-collapsed');
|
|
this.queueElement.style.display = 'none';
|
|
this.queueElement.style.width = '0px';
|
|
this.startSplitDrag(
|
|
{ clientX: startX, preventDefault: () => {} },
|
|
{ startWidth: 0, edgeOpen: true, previousCursor, startCollapsedPreview: true }
|
|
);
|
|
},
|
|
|
|
// Begin split drag for resizable panels
|
|
startSplitDrag(e, { startWidth = null, edgeOpen = false, previousCursor = null, startCollapsedPreview = false } = {}) {
|
|
try {
|
|
e?.preventDefault?.();
|
|
if (!this.queueElement) return;
|
|
const rect = this.queueElement.getBoundingClientRect();
|
|
const body = document.getElementById('model-resolver-body');
|
|
const bodyRect = body ? body.getBoundingClientRect() : { width: window.innerWidth };
|
|
this._splitDragging = true;
|
|
body?.classList.add('mr-is-resizing-queue');
|
|
this._splitStart = {
|
|
x: e.clientX,
|
|
startWidth: Number.isFinite(startWidth) ? startWidth : rect.width,
|
|
containerWidth: bodyRect.width
|
|
};
|
|
this._lastSplitDragApply = 0;
|
|
this._splitPreviewCollapsed = !!startCollapsedPreview;
|
|
this._splitEdgeOpen = !!edgeOpen;
|
|
this._splitEdgeOpenedPastThreshold = false;
|
|
this._splitPreviousCursor = previousCursor;
|
|
this._prevUserSelect = document.body.style.userSelect;
|
|
document.body.style.userSelect = 'none';
|
|
this._onSplitMove = (ev) => this.onSplitDrag(ev);
|
|
this._onSplitUp = () => this.endSplitDrag();
|
|
document.addEventListener('mousemove', this._onSplitMove);
|
|
document.addEventListener('mouseup', this._onSplitUp, { once: true });
|
|
} catch (err) { /* ignore */ }
|
|
},
|
|
|
|
onSplitDrag(e) {
|
|
if (!this._splitDragging || !this._splitStart || !this.queueElement) return;
|
|
const dx = e.clientX - this._splitStart.x;
|
|
let newW = this._splitStart.startWidth - dx;
|
|
const minW = this._splitEdgeOpen ? 0 : 56;
|
|
const maxW = Math.max(minW, Math.floor(this._splitStart.containerWidth - 360));
|
|
if (newW < minW) newW = minW;
|
|
if (newW > maxW) newW = maxW;
|
|
this._pendingSplitWidth = Math.round(newW);
|
|
if (this._splitEdgeOpen && this._pendingSplitWidth > 140) {
|
|
this._splitEdgeOpenedPastThreshold = true;
|
|
if (this.queueCollapsed) {
|
|
this.queueCollapsed = false;
|
|
try { localStorage.setItem('model_resolver_queue_collapsed', '0'); } catch (error) { }
|
|
this.updateQueueToggleIcon();
|
|
this.updateQueuePanel();
|
|
}
|
|
}
|
|
const previewCollapsed = this._splitEdgeOpen
|
|
? (!this._splitEdgeOpenedPastThreshold || this._pendingSplitWidth <= 140)
|
|
: this._pendingSplitWidth <= 140;
|
|
if (previewCollapsed !== this._splitPreviewCollapsed) {
|
|
this._splitPreviewCollapsed = previewCollapsed;
|
|
const body = document.getElementById('model-resolver-body');
|
|
body?.classList.toggle('mr-queue-preview-collapsed', previewCollapsed);
|
|
this.queueElement.style.display = previewCollapsed ? 'none' : '';
|
|
}
|
|
const now = performance.now();
|
|
if (this._lastSplitDragApply && now - this._lastSplitDragApply < 33) return;
|
|
this._lastSplitDragApply = now;
|
|
if (this._splitDragFrame) return;
|
|
this._splitDragFrame = requestAnimationFrame(() => {
|
|
this._splitDragFrame = null;
|
|
if (!this._splitDragging || !this.queueElement || !this._pendingSplitWidth || this._splitPreviewCollapsed) return;
|
|
this.queueElement.style.width = `${this._pendingSplitWidth}px`;
|
|
});
|
|
},
|
|
|
|
endSplitDrag() {
|
|
if (!this._splitDragging) return;
|
|
this._splitDragging = false;
|
|
if (this._splitDragFrame) {
|
|
cancelAnimationFrame(this._splitDragFrame);
|
|
this._splitDragFrame = null;
|
|
}
|
|
if (this.queueElement && this._pendingSplitWidth && !this._splitPreviewCollapsed) {
|
|
this.queueElement.style.width = `${this._pendingSplitWidth}px`;
|
|
}
|
|
const body = document.getElementById('model-resolver-body');
|
|
body?.classList.remove('mr-is-resizing-queue');
|
|
body?.classList.remove('mr-queue-preview-collapsed');
|
|
document.removeEventListener('mousemove', this._onSplitMove);
|
|
if (this._splitEdgeOpen && !this._pendingSplitWidth) {
|
|
try {
|
|
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
|
|
this.queueElement.style.width = `${restoreWidth}px`;
|
|
this.queueElement.style.display = '';
|
|
if (this.splitterElement) this.splitterElement.style.display = '';
|
|
localStorage.setItem('model_resolver_split_w', String(restoreWidth));
|
|
} catch (e) { }
|
|
this.queueCollapsed = false;
|
|
try { localStorage.setItem('model_resolver_queue_collapsed', '0'); } catch (e) { }
|
|
this.updateQueueVisibility();
|
|
this.updateQueuePanel();
|
|
this._splitEdgeOpen = false;
|
|
this._pendingSplitWidth = null;
|
|
this._lastSplitDragApply = 0;
|
|
this._splitPreviewCollapsed = false;
|
|
try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) { }
|
|
try { document.body.style.cursor = this._splitPreviousCursor || ''; } catch (e) { }
|
|
this._splitPreviousCursor = null;
|
|
this._splitEdgeOpenedPastThreshold = false;
|
|
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
|
|
return;
|
|
}
|
|
const shouldCollapse = !!this._splitPreviewCollapsed;
|
|
if (shouldCollapse) {
|
|
try {
|
|
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
|
|
this.queueElement.style.width = `${restoreWidth}px`;
|
|
this.queueElement.style.display = '';
|
|
localStorage.setItem('model_resolver_split_w', String(restoreWidth));
|
|
} catch (e) { }
|
|
this._pendingSplitWidth = null;
|
|
this._lastSplitDragApply = 0;
|
|
this._splitPreviewCollapsed = false;
|
|
this._splitEdgeOpen = false;
|
|
this._splitEdgeOpenedPastThreshold = false;
|
|
try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) { }
|
|
try { document.body.style.cursor = this._splitPreviousCursor || ''; } catch (e) { }
|
|
this._splitPreviousCursor = null;
|
|
this.setQueueCollapsed(true);
|
|
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
|
|
return;
|
|
}
|
|
if (this.queueElement) this.queueElement.style.display = '';
|
|
try {
|
|
const rect = this.queueElement.getBoundingClientRect();
|
|
localStorage.setItem('model_resolver_split_w', String(Math.round(rect.width)));
|
|
} catch (e) { }
|
|
this._pendingSplitWidth = null;
|
|
this._lastSplitDragApply = 0;
|
|
this._splitPreviewCollapsed = false;
|
|
this._splitEdgeOpen = false;
|
|
this._splitEdgeOpenedPastThreshold = false;
|
|
try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) { }
|
|
if (this._splitPreviousCursor !== null) {
|
|
try { document.body.style.cursor = this._splitPreviousCursor || ''; } catch (e) { }
|
|
this._splitPreviousCursor = null;
|
|
}
|
|
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
|
|
},
|
|
|
|
/**
|
|
* Queue a resolution for later batch apply
|
|
*/
|
|
queueResolution(missing, resolvedModel) {
|
|
if (!resolvedModel) {
|
|
this.showNotification('No model selected', 'error');
|
|
return;
|
|
}
|
|
|
|
const resolution = {
|
|
node_id: missing.node_id,
|
|
widget_index: missing.widget_index,
|
|
resolved_path: resolvedModel.path,
|
|
category: missing.category,
|
|
resolved_model: resolvedModel,
|
|
original_path: missing.original_path,
|
|
subgraph_id: missing.subgraph_id,
|
|
is_top_level: missing.is_top_level,
|
|
node_type: missing.node_type,
|
|
node_label: missing.subgraph_name || missing.node_type
|
|
};
|
|
|
|
const key = `${resolution.node_id}:${resolution.widget_index}:${resolution.subgraph_id || ''}:${resolution.is_top_level ? 'T' : 'F'}`;
|
|
if (this.pendingIndex.has(key)) {
|
|
// replace existing selection for this slot
|
|
const idx = this.pendingIndex.get(key);
|
|
this.pendingResolutions[idx] = resolution;
|
|
} else {
|
|
this.pendingIndex.set(key, this.pendingResolutions.length);
|
|
this.pendingResolutions.push(resolution);
|
|
}
|
|
this.savePendingQueueForActiveWorkflow();
|
|
|
|
// Update selected bar UI
|
|
this.updateSelectedBarForMissing?.(missing);
|
|
this.updateQueuePanel();
|
|
this.updateApplyPendingButton();
|
|
},
|
|
|
|
/**
|
|
* Apply all pending resolutions in batch
|
|
*/
|
|
async applyPendingResolutions() {
|
|
const list = this.pendingResolutions || [];
|
|
if (!list.length) {
|
|
this.showNotification('No selections queued', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const workflow = this.getCurrentWorkflow();
|
|
if (!workflow) {
|
|
this.showNotification('No workflow loaded', 'error');
|
|
return;
|
|
}
|
|
|
|
const response = await api.fetchApi('/model_resolver/resolve', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ workflow, resolutions: list })
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
await this.updateWorkflowInComfyUI(data.workflow);
|
|
this.showNotification(`✓ Linked ${list.length} selection${list.length>1?'s':''}`, 'success');
|
|
// Clear queue and refresh analysis
|
|
this.pendingResolutions = [];
|
|
this.pendingIndex = new Map();
|
|
this.savePendingQueueForActiveWorkflow();
|
|
this.updateApplyPendingButton();
|
|
this.updateQueuePanel();
|
|
await this.loadWorkflowData(data.workflow, { force: true });
|
|
} else {
|
|
this.showNotification('Failed to apply selections: ' + (data.error || 'Unknown error'), 'error');
|
|
}
|
|
} catch (e) {
|
|
console.error('Model Resolver: applyPendingResolutions error', e);
|
|
this.showNotification('Error applying selections: ' + e.message, 'error');
|
|
}
|
|
},
|
|
|
|
updateApplyPendingButton() {
|
|
if (!this.applyPendingBtn) return;
|
|
const count = this.pendingResolutions?.length || 0;
|
|
const isEmpty = count === 0;
|
|
const tooltip = count > 0
|
|
? `Apply ${count} queued model link${count > 1 ? 's' : ''} to the current workflow.`
|
|
: 'Apply the model links you selected from local matches or search results.';
|
|
this.applyPendingBtn.textContent = `Apply Selected (${count})`;
|
|
this.setTooltip(this.applyPendingBtn, tooltip);
|
|
this.applyPendingBtn.disabled = isEmpty;
|
|
this.applyPendingBtn.setAttribute('aria-disabled', String(isEmpty));
|
|
this.applyPendingBtn.classList.toggle('mr-btn-is-disabled', isEmpty);
|
|
this.updateBatchFooterButtons();
|
|
},
|
|
|
|
updateBatchFooterButtons() {
|
|
const missingModels = this.missingModels || [];
|
|
const selectedCount = this.getSelectedMissingModels().length;
|
|
const totalCount = missingModels.length;
|
|
const pendingCount = this.pendingResolutions?.length || 0;
|
|
const activeCount = Object.keys(this.activeDownloads || {}).length;
|
|
const downloadableSelected = this.getMissingWithDownloadSources(this.getSelectedMissingModels()).length;
|
|
const downloadableAll = this.getMissingWithDownloadSources(missingModels).length;
|
|
|
|
if (this.selectMenuButton) {
|
|
this.selectMenuButton.textContent = `Select (${selectedCount}/${totalCount})`;
|
|
this.selectMenuButton.classList.toggle('mr-btn-is-disabled', totalCount === 0);
|
|
}
|
|
if (this.searchMenuButton) {
|
|
this.searchMenuButton.textContent = this.batchSearchRunning
|
|
? (this.batchSearchCancelRequested ? 'Stopping...' : 'Searching...')
|
|
: 'Search';
|
|
this.searchMenuButton.classList.toggle('mr-btn-is-disabled', totalCount === 0 && !this.batchSearchRunning);
|
|
}
|
|
if (this.downloadMenuButton) {
|
|
const label = activeCount > 0 ? `Cancel Downloads (${activeCount})` : 'Download';
|
|
this.downloadMenuButton.textContent = label;
|
|
this.downloadMenuButton.classList.toggle('mr-btn-danger', activeCount > 0);
|
|
this.downloadMenuButton.classList.toggle('mr-btn-download', activeCount === 0);
|
|
this.downloadMenuButton.classList.toggle('mr-btn-is-disabled', activeCount === 0 && downloadableAll === 0);
|
|
this.setTooltip(
|
|
this.downloadMenuButton,
|
|
activeCount > 0
|
|
? `Cancel ${activeCount} active download${activeCount > 1 ? 's' : ''}.`
|
|
: `${downloadableSelected || downloadableAll} missing model${(downloadableSelected || downloadableAll) === 1 ? '' : 's'} currently have download sources.`
|
|
);
|
|
}
|
|
if (this.applyPendingBtn) {
|
|
this.applyPendingBtn.textContent = `Apply Selected (${pendingCount})`;
|
|
}
|
|
if (this.linkMenuButton) {
|
|
this.linkMenuButton.classList.toggle('mr-btn-is-disabled', totalCount === 0 && pendingCount === 0);
|
|
}
|
|
this.updateFooterMenuItemVisibility?.();
|
|
},
|
|
|
|
getMissingModelsForPendingResolutions() {
|
|
const pending = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : [];
|
|
if (!pending.length) return [];
|
|
|
|
const pendingKeys = new Set(pending.map(resolution => this.getMissingModelKey(resolution)));
|
|
return (this.missingModels || []).filter(missing => pendingKeys.has(this.getMissingModelKey(missing)));
|
|
},
|
|
|
|
getQueueExactLocalMatchPreviewModels() {
|
|
const selected = this.getSelectedMissingModels();
|
|
const targets = selected.length ? selected : (this.missingModels || []);
|
|
return targets.filter(missing => this.getBestLocalMatch(missing, 100)?.model);
|
|
},
|
|
|
|
getDownloadableBatchPreviewModels(mode = 'all') {
|
|
const targets = mode === 'selected'
|
|
? this.getSelectedMissingModels()
|
|
: (this.missingModels || []);
|
|
|
|
return targets.filter(missing => {
|
|
if (this.getBestLocalMatch(missing, 100)) return false;
|
|
return Boolean(this.getBestDownloadSourceForMissing(missing)?.url);
|
|
});
|
|
},
|
|
|
|
getActiveDownloadMissingModels() {
|
|
const activeKeys = new Set(
|
|
Object.values(this.activeDownloads || {})
|
|
.map(info => info?.missing)
|
|
.filter(Boolean)
|
|
.map(missing => this.getMissingModelKey(missing))
|
|
);
|
|
if (!activeKeys.size) return [];
|
|
|
|
return (this.missingModels || []).filter(missing => activeKeys.has(this.getMissingModelKey(missing)));
|
|
},
|
|
|
|
previewFooterMenuModels(models = []) {
|
|
const keys = new Set(
|
|
(Array.isArray(models) ? models : [])
|
|
.filter(Boolean)
|
|
.map(missing => this.getMissingModelKey(missing))
|
|
);
|
|
|
|
this.contentElement?.querySelectorAll?.('.mr-missing-list-row')?.forEach(row => {
|
|
const key = row.getAttribute('data-missing-key');
|
|
row.classList.toggle('is-menu-preview', Boolean(key && keys.has(key)));
|
|
});
|
|
},
|
|
|
|
clearFooterMenuPreview() {
|
|
this.contentElement?.querySelectorAll?.('.mr-missing-list-row.is-menu-preview')?.forEach(row => {
|
|
row.classList.remove('is-menu-preview');
|
|
});
|
|
},
|
|
|
|
createFooterMenu(name, label, items = [], buttonClass = 'mr-btn-secondary') {
|
|
const button = $el(`button.mr-btn.${buttonClass}.mr-footer-btn.mr-footer-menu-button`, {
|
|
type: 'button',
|
|
'aria-haspopup': 'menu',
|
|
'aria-expanded': 'false',
|
|
onclick: (event) => {
|
|
event.stopPropagation();
|
|
this.hideTooltip?.();
|
|
if (button.classList.contains('mr-btn-is-disabled')) {
|
|
if (name === 'download') {
|
|
this.showNotification('No missing models have downloadable sources yet.', 'info');
|
|
} else {
|
|
this.showNotification('No missing models available.', 'info');
|
|
}
|
|
return;
|
|
}
|
|
this.toggleFooterMenu(name);
|
|
}
|
|
}, [$el("span", { textContent: label })]);
|
|
|
|
const menuChildren = items.map(item => {
|
|
const isDivider = item === 'divider' || item?.type === 'divider';
|
|
let element;
|
|
if (isDivider) {
|
|
element = $el("div.mr-footer-menu-divider", {});
|
|
} else if (item?.type === 'checkbox') {
|
|
const input = $el("input.mr-footer-menu-checkbox-input", {
|
|
type: 'checkbox',
|
|
checked: Boolean(item.checked?.())
|
|
});
|
|
element = $el("label.mr-footer-menu-item.mr-footer-menu-checkbox", {
|
|
role: 'menuitemcheckbox',
|
|
'aria-checked': input.checked ? 'true' : 'false',
|
|
onclick: (event) => {
|
|
event.stopPropagation();
|
|
}
|
|
}, [
|
|
input,
|
|
$el("span", { textContent: item.label })
|
|
]);
|
|
input.addEventListener('change', () => {
|
|
item.action?.(input.checked);
|
|
element.setAttribute('aria-checked', input.checked ? 'true' : 'false');
|
|
});
|
|
} else {
|
|
const attributes = {
|
|
type: 'button',
|
|
role: 'menuitem',
|
|
onclick: (event) => {
|
|
event.stopPropagation();
|
|
if (element.disabled || element.classList.contains('mr-btn-is-disabled')) return;
|
|
item.action();
|
|
}
|
|
};
|
|
if (item.id) attributes.id = item.id;
|
|
if (item.ariaLabel) attributes['aria-label'] = item.ariaLabel;
|
|
element = $el("button.mr-footer-menu-item", attributes, [$el("span", { textContent: item.label })]);
|
|
}
|
|
|
|
if (item?.className) {
|
|
element.classList.add(...String(item.className).split(/\s+/).filter(Boolean));
|
|
}
|
|
if (item?.refName) {
|
|
this[item.refName] = element;
|
|
}
|
|
if (!isDivider && typeof item?.previewModels === 'function') {
|
|
const showPreview = () => {
|
|
if (element.disabled || element.classList.contains('mr-btn-is-disabled')) {
|
|
this.clearFooterMenuPreview();
|
|
return;
|
|
}
|
|
try {
|
|
this.previewFooterMenuModels(item.previewModels());
|
|
} catch (error) {
|
|
console.warn('Model Resolver: footer menu preview failed:', error);
|
|
this.clearFooterMenuPreview();
|
|
}
|
|
};
|
|
element.addEventListener('mouseenter', showPreview);
|
|
element.addEventListener('focus', showPreview);
|
|
element.addEventListener('mouseleave', () => this.clearFooterMenuPreview());
|
|
element.addEventListener('blur', () => this.clearFooterMenuPreview());
|
|
}
|
|
if (typeof item?.visibleWhen === 'function' || typeof item?.disabledWhen === 'function') {
|
|
if (!this.footerMenuItems) this.footerMenuItems = new Map();
|
|
if (!this.footerMenuItems.has(name)) this.footerMenuItems.set(name, []);
|
|
this.footerMenuItems.get(name).push({
|
|
element,
|
|
visibleWhen: item.visibleWhen,
|
|
disabledWhen: item.disabledWhen
|
|
});
|
|
if (typeof item.visibleWhen === 'function') {
|
|
element.style.display = item.visibleWhen() ? '' : 'none';
|
|
}
|
|
if (typeof item.disabledWhen === 'function') {
|
|
const disabled = item.disabledWhen();
|
|
element.disabled = disabled;
|
|
element.setAttribute('aria-disabled', String(disabled));
|
|
element.classList.toggle('mr-btn-is-disabled', disabled);
|
|
}
|
|
}
|
|
return element;
|
|
});
|
|
|
|
const menu = $el("div.mr-footer-menu", { role: 'menu' }, menuChildren);
|
|
|
|
this.footerMenuButtons.set(name, button);
|
|
this.footerMenus.set(name, menu);
|
|
|
|
return $el("div.mr-footer-menu-wrap", {}, [button, menu]);
|
|
},
|
|
|
|
updateFooterMenuItemVisibility() {
|
|
if (!this.footerMenuItems) return;
|
|
for (const entries of this.footerMenuItems.values()) {
|
|
for (const entry of entries) {
|
|
if (typeof entry.visibleWhen === 'function') {
|
|
entry.element.style.display = entry.visibleWhen() ? '' : 'none';
|
|
}
|
|
if (typeof entry.disabledWhen === 'function') {
|
|
const disabled = entry.disabledWhen();
|
|
entry.element.disabled = disabled;
|
|
entry.element.setAttribute('aria-disabled', String(disabled));
|
|
entry.element.classList.toggle('mr-btn-is-disabled', disabled);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
createFooter() {
|
|
this.footerMenus = new Map();
|
|
this.footerMenuButtons = new Map();
|
|
this.footerMenuItems = new Map();
|
|
this.linkMenuButton = null;
|
|
this.linkMenuWrap = null;
|
|
this.queueExactButton = null;
|
|
this.applyPendingBtn = null;
|
|
this.clearSelectedButton = null;
|
|
this.autoResolveButton = null;
|
|
this.downloadMenuButton = null;
|
|
this.downloadAllButton = null;
|
|
|
|
const selectMenu = this.createFooterMenu('select', 'Select (0/0)', [
|
|
{ label: 'Select All', previewModels: () => this.missingModels || [], action: () => this.selectBatchMissingModels('all') },
|
|
{ label: 'Select None', previewModels: () => this.getSelectedMissingModels(), action: () => this.selectBatchMissingModels('none') },
|
|
{
|
|
label: 'Invert Selection',
|
|
previewModels: () => {
|
|
const selectedKeys = this.batchSelectedMissingKeys || new Set();
|
|
return (this.missingModels || []).filter(missing => !selectedKeys.has(this.getMissingModelKey(missing)));
|
|
},
|
|
action: () => this.selectBatchMissingModels('invert')
|
|
},
|
|
'divider',
|
|
{ label: 'Select Exact Local', previewModels: () => this.getMissingWithExactLocalMatches(), action: () => this.selectBatchMissingModels('exact') },
|
|
{ label: 'Select No Exact Local', previewModels: () => this.getMissingWithoutExactLocalMatches(), action: () => this.selectBatchMissingModels('no_exact') },
|
|
{ label: 'Select Partial Local', previewModels: () => this.getMissingWithPartialLocalMatches(), action: () => this.selectBatchMissingModels('partial') },
|
|
{ label: 'Select No Local Match', previewModels: () => this.getMissingWithoutLocalMatches(), action: () => this.selectBatchMissingModels('no_local') },
|
|
'divider',
|
|
{ label: 'Select With Download Source', previewModels: () => this.getMissingWithDownloadSources(), action: () => this.selectBatchMissingModels('downloadable') },
|
|
{ label: 'Select Without Download Source', previewModels: () => this.getMissingWithoutDownloadSources(), action: () => this.selectBatchMissingModels('no_download') },
|
|
'divider',
|
|
{ label: 'Select Searched', previewModels: () => this.getSearchedMissingModels(), action: () => this.selectBatchMissingModels('searched') },
|
|
{ label: 'Select Unsearched', previewModels: () => this.getUnsearchedMissingModels(), action: () => this.selectBatchMissingModels('unsearched') }
|
|
]);
|
|
this.selectMenuButton = this.footerMenuButtons.get('select');
|
|
this.selectMenuWrap = selectMenu;
|
|
|
|
const hasSelectedMissingModels = () => this.getSelectedMissingModels().length > 0;
|
|
const searchBatchOptions = () => ({ forceSearch: Boolean(this.forceBatchSearch) });
|
|
const searchMenu = this.createFooterMenu('search', 'Search', [
|
|
{ label: 'Stop Searching', className: 'mr-footer-menu-danger', visibleWhen: () => this.batchSearchRunning, action: () => this.stopBatchSearch() },
|
|
{ type: 'divider', visibleWhen: () => this.batchSearchRunning },
|
|
{
|
|
type: 'checkbox',
|
|
label: 'Force search (ignore cache)',
|
|
visibleWhen: () => !this.batchSearchRunning,
|
|
checked: () => Boolean(this.forceBatchSearch),
|
|
action: (checked) => {
|
|
this.forceBatchSearch = Boolean(checked);
|
|
}
|
|
},
|
|
{ type: 'divider', visibleWhen: () => !this.batchSearchRunning },
|
|
{ label: 'Search Selected', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'all', searchBatchOptions()) },
|
|
{ label: 'Search All Missing', visibleWhen: () => !this.batchSearchRunning, previewModels: () => this.missingModels || [], action: () => this.searchMissingBatch('all', 'all', searchBatchOptions()) },
|
|
{ label: 'Search Unsearched', visibleWhen: () => !this.batchSearchRunning, previewModels: () => this.getUnsearchedMissingModels(), action: () => this.searchMissingBatch('unsearched', 'all', searchBatchOptions()) },
|
|
{ type: 'divider', visibleWhen: () => !this.batchSearchRunning },
|
|
{ label: 'Selected: Local Database', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'local', searchBatchOptions()) },
|
|
{ label: 'Selected: CivitAI', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'civitai', searchBatchOptions()) },
|
|
{ label: 'Selected: HuggingFace', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'huggingface', searchBatchOptions()) },
|
|
{ label: 'Selected: CivArchive', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'civarchive', searchBatchOptions()) },
|
|
{ label: 'Selected: LoRA Manager Archive', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'lora_manager_archive', searchBatchOptions()) }
|
|
]);
|
|
this.searchMenuButton = this.footerMenuButtons.get('search');
|
|
this.searchMenuWrap = searchMenu;
|
|
|
|
const hasExactLocalMatches = () => this.getMissingWithExactLocalMatches(this.missingModels || []).length > 0;
|
|
const linkMenu = this.createFooterMenu('link', 'Link', [
|
|
{
|
|
label: 'Queue Exact',
|
|
refName: 'queueExactButton',
|
|
ariaLabel: 'Queue exact local matches',
|
|
previewModels: () => this.getQueueExactLocalMatchPreviewModels(),
|
|
action: () => this.queueExactLocalMatchesBatch('selected')
|
|
},
|
|
{
|
|
label: 'Apply Selected (0)',
|
|
id: 'apply-pending-resolutions',
|
|
refName: 'applyPendingBtn',
|
|
ariaLabel: 'Apply selected model links',
|
|
disabledWhen: () => (this.pendingResolutions?.length || 0) === 0,
|
|
previewModels: () => this.getMissingModelsForPendingResolutions(),
|
|
action: () => {
|
|
if ((this.pendingResolutions?.length || 0) === 0) return;
|
|
this.applyPendingResolutions();
|
|
}
|
|
},
|
|
{
|
|
label: 'Clear All Selected',
|
|
refName: 'clearSelectedButton',
|
|
ariaLabel: 'Clear all queued selected model links',
|
|
disabledWhen: () => (this.pendingResolutions?.length || 0) === 0,
|
|
previewModels: () => this.getMissingModelsForPendingResolutions(),
|
|
action: () => {
|
|
this.clearAllQueued();
|
|
this.closeFooterMenus();
|
|
}
|
|
},
|
|
{ type: 'divider', visibleWhen: hasExactLocalMatches },
|
|
{
|
|
label: 'Auto-Link 100%',
|
|
refName: 'autoResolveButton',
|
|
ariaLabel: 'Auto-link all 100 percent local matches',
|
|
visibleWhen: hasExactLocalMatches,
|
|
previewModels: () => this.getMissingWithExactLocalMatches(),
|
|
action: () => this.autoResolve100Percent()
|
|
}
|
|
]);
|
|
this.linkMenuButton = this.footerMenuButtons.get('link');
|
|
this.linkMenuWrap = linkMenu;
|
|
this.linkMenuButton.setAttribute('aria-label', 'Local link actions');
|
|
this.setTooltip(this.linkMenuButton, 'Queue exact local matches, apply queued links, or auto-link 100% matches.');
|
|
this.setTooltip(this.queueExactButton, 'Queue exact local matches for the selected rows, or all exact matches if nothing is selected.');
|
|
this.setTooltip(this.applyPendingBtn, 'Apply the model links you selected from local matches or search results.');
|
|
this.setTooltip(this.clearSelectedButton, 'Clear all queued model links without changing the workflow.');
|
|
this.setTooltip(this.autoResolveButton, 'Automatically link all missing models with a 100% local match.');
|
|
this.updateApplyPendingButton();
|
|
|
|
const downloadMenu = this.createFooterMenu('download', 'Download', [
|
|
{ label: 'Download Selected', disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getDownloadableBatchPreviewModels('selected'), action: () => this.downloadMissingBatch('selected') },
|
|
{ label: 'Download All With Sources', previewModels: () => this.getDownloadableBatchPreviewModels('all'), action: () => this.downloadMissingBatch('all') },
|
|
'divider',
|
|
{ label: 'Cancel Downloads', previewModels: () => this.getActiveDownloadMissingModels(), action: () => { this.closeFooterMenus(); this.cancelAllDownloads(); } }
|
|
], 'mr-btn-download');
|
|
this.downloadMenuButton = this.footerMenuButtons.get('download');
|
|
this.downloadMenuWrap = downloadMenu;
|
|
this.downloadAllButton = this.downloadMenuButton;
|
|
this.downloadAllButton.setAttribute('aria-label', 'Download missing models');
|
|
this.setTooltip(this.downloadAllButton, 'Download selected models or all missing models with known download sources.');
|
|
|
|
return $el("div.mr-footer", {}, [
|
|
selectMenu,
|
|
searchMenu,
|
|
linkMenu,
|
|
downloadMenu
|
|
]);
|
|
},
|
|
|
|
animateTabContentTransition() {
|
|
if (!this.contentElement?.animate) return;
|
|
|
|
try {
|
|
this.contentElement.animate(
|
|
[
|
|
{ opacity: 0.72, transform: 'translateY(7px)' },
|
|
{ opacity: 1, transform: 'translateY(0)' }
|
|
],
|
|
{
|
|
duration: 180,
|
|
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)'
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.warn('Model Resolver: tab content animation failed', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle click on Download All / Cancel All button
|
|
*/
|
|
handleDownloadAllClick() {
|
|
if (Object.keys(this.activeDownloads).length > 0) {
|
|
// Cancel all active downloads
|
|
this.cancelAllDownloads();
|
|
} else {
|
|
// Start downloading all missing
|
|
this.downloadAllMissing();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cancel all active downloads
|
|
*/
|
|
async cancelAllDownloads() {
|
|
const downloadIds = Object.keys(this.activeDownloads);
|
|
if (downloadIds.length === 0) return;
|
|
|
|
this.showNotification(`Cancelling ${downloadIds.length} download${downloadIds.length > 1 ? 's' : ''}...`, 'info');
|
|
|
|
for (const downloadId of downloadIds) {
|
|
try {
|
|
await api.fetchApi(`/model_resolver/cancel/${downloadId}`, {
|
|
method: 'POST'
|
|
});
|
|
} catch (error) {
|
|
console.error('Model Resolver: Error cancelling download:', error);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update the Download All button state based on active downloads
|
|
*/
|
|
updateDownloadAllButtonState() {
|
|
if (!this.downloadAllButton) return;
|
|
if (this.downloadMenuButton && this.downloadAllButton === this.downloadMenuButton) {
|
|
this.updateBatchFooterButtons();
|
|
return;
|
|
}
|
|
|
|
const activeCount = Object.keys(this.activeDownloads).length;
|
|
if (activeCount > 0) {
|
|
this.downloadAllButton.innerHTML = `<span class="mr-btn-icon">✕</span> Cancel All (${activeCount})`;
|
|
this.downloadAllButton.setAttribute(
|
|
'data-tooltip',
|
|
`Cancel ${activeCount} active download${activeCount > 1 ? 's' : ''}.`
|
|
);
|
|
this.downloadAllButton.setAttribute('aria-label', 'Cancel all active downloads');
|
|
this.downloadAllButton.classList.remove('mr-btn-download');
|
|
this.downloadAllButton.classList.add('mr-btn-danger');
|
|
} else {
|
|
this.downloadAllButton.innerHTML = `<span class="mr-btn-icon">☁</span> Download All Missing`;
|
|
this.downloadAllButton.setAttribute(
|
|
'data-tooltip',
|
|
'Download every missing model that has a known download source.'
|
|
);
|
|
this.downloadAllButton.setAttribute('aria-label', 'Download all missing models');
|
|
this.downloadAllButton.classList.remove('mr-btn-danger');
|
|
this.downloadAllButton.classList.add('mr-btn-download');
|
|
}
|
|
}
|
|
};
|