Include known download source in search results, dedupe rows, and add download wiring

Add getDownloadSourceTableRow to render known/download-source rows
Include known download source in search results and prevent duplicates
Add wireSearchDownloadButtons to attach download handlers
Allow custom search button text and render download target controls
Minor UI/formatting fixes and URN refresh alignment
This commit is contained in:
Dariusz L
2026-05-26 16:27:06 +02:00
parent ffd7d548f2
commit 5bcfd12ff5
+94 -22
View File
@@ -482,6 +482,49 @@ class LinkerManagerDialog extends ComfyDialog {
return { label: fallbackLabel, className: fallbackClass };
}
getDownloadSourceTableRow(missing, downloadSource = {}) {
if (!downloadSource?.url) return null;
const originalFilename = missing.original_path?.split('/').pop()?.split('\\').pop() || '';
const downloadFilename = downloadSource.filename || originalFilename || 'model';
const isFromWorkflow = downloadSource.url_source === 'workflow';
const source = downloadSource.source || (isFromWorkflow ? 'workflow' : 'online');
const sourceLabels = {
popular: 'Popular',
model_list: 'Local Database',
huggingface: 'HuggingFace',
civitai: 'CivitAI',
workflow: 'Workflow',
online: 'Online'
};
const sourceLabel = isFromWorkflow ? 'Workflow URL' : (sourceLabels[source] || source);
const sourceKey = isFromWorkflow
? 'workflow-url'
: String(source).replace(/_/g, '-');
const sourceSecondary = isFromWorkflow && sourceLabels[source] && source !== 'workflow'
? sourceLabels[source]
: '';
const modelUrl = downloadSource.model_url
|| downloadSource.workflow_model_url
|| this.getModelCardUrl(downloadSource.url);
return {
sourceKey,
sourceLabel,
model: downloadSource.name || downloadFilename,
filename: downloadFilename,
secondary: sourceSecondary || (downloadSource.name && downloadSource.name !== downloadFilename ? downloadFilename : ''),
match: isFromWorkflow
? { label: 'Provided', className: 'strong' }
: this.getSearchResultMatchDisplay(downloadSource, 'Known', 'strong'),
size: this.formatSearchResultSize(downloadSource),
downloadUrl: downloadSource.url,
downloadFilename,
category: downloadSource.directory || downloadSource.category || missing.category || 'checkpoints',
openUrl: modelUrl
};
}
renderSearchResultsTable(rows = []) {
if (!rows.length) return '';
@@ -570,13 +613,14 @@ class LinkerManagerDialog extends ComfyDialog {
return html;
}
renderSearchControls(missing) {
renderSearchControls(missing, options = {}) {
const searchSourcesId = `search-sources-${missing.node_id}-${missing.widget_index}`;
const searchSourceSelectId = `search-source-select-${missing.node_id}-${missing.widget_index}`;
const buttonText = options.buttonText || 'Search';
let html = `<div id="${searchSourcesId}" class="ml-search-source-bar">`;
html += `<button id="search-${missing.node_id}-${missing.widget_index}" class="ml-btn ml-btn-link">`;
html += `${this.getSearchIconHtml()} Search`;
html += `${this.getSearchIconHtml()} ${buttonText}`;
html += `</button>`;
html += `<div class="ml-search-source-picker">`;
html += `<label class="ml-search-source-picker-label" for="${searchSourceSelectId}">Source</label>`;
@@ -3828,6 +3872,12 @@ class LinkerManagerDialog extends ComfyDialog {
this.downloadModel(missing);
});
}
const searchResultsId = `search-results-${missing.node_id}-${missing.widget_index}`;
const searchResultsDiv = container.querySelector(`#${searchResultsId}`);
if (searchResultsDiv) {
this.wireSearchDownloadButtons(searchResultsDiv, missing);
}
// Attach search button listener
const searchBtnId = `search-${missing.node_id}-${missing.widget_index}`;
@@ -4092,11 +4142,17 @@ class LinkerManagerDialog extends ComfyDialog {
html += `</div>`;
html += `</div>`;
} else if (downloadSource && downloadSource.url) {
html += this.renderDownloadSourceSection(missing, downloadSource);
html += `<div class="ml-download-section">`;
html += this.renderSearchControls(missing);
html += this.renderSearchControls(missing, { buttonText: 'Search Again' });
html += this.renderDownloadTargetControls(
missing,
downloadSource.directory || downloadSource.category || missing.category || 'checkpoints'
);
html += `</div>`;
const downloadSourceRow = this.getDownloadSourceTableRow(missing, downloadSource);
html += `<div id="search-results-${missing.node_id}-${missing.widget_index}" class="ml-search-results" style="display: block;">`;
html += this.renderSearchResultsTable(downloadSourceRow ? [downloadSourceRow] : []);
html += `</div>`;
html += `<div id="search-results-${missing.node_id}-${missing.widget_index}" class="ml-search-results"></div>`;
} else if (missing.is_urn) {
html += `<div id="${urnDownloadId}" class="ml-download-section">`;
html += `<div class="ml-download-info">Resolving CivitAI download for this URN...</div>`;
@@ -5043,7 +5099,7 @@ class LinkerManagerDialog extends ComfyDialog {
if (this.contentElement) {
this.wireDownloadTargetAutocomplete(this.contentElement, missing);
}
this.refreshUrnLocalMatches(missing);
this.refreshUrnLocalMatches(missing);
}
} else if (downloadEl) {
downloadEl.innerHTML = `<div class="ml-download-info">Unable to resolve direct download for this URN.</div>`;
@@ -5075,6 +5131,20 @@ class LinkerManagerDialog extends ComfyDialog {
}
}
wireSearchDownloadButtons(container, missing) {
if (!container) return;
const downloadBtns = container.querySelectorAll('.search-download-btn');
downloadBtns.forEach(btn => {
btn.addEventListener('click', () => {
const url = btn.dataset.url;
const filename = btn.dataset.filename;
const category = btn.dataset.category;
this.downloadFromSearch(missing, url, filename, category, btn);
});
});
}
/**
* Display search results
*/
@@ -5087,7 +5157,8 @@ class LinkerManagerDialog extends ComfyDialog {
const hfResult = results.huggingface ? (Array.isArray(results.huggingface) ? results.huggingface[0] : results.huggingface) : null;
const civitaiResult = results.civitai ? (Array.isArray(results.civitai) ? results.civitai[0] : results.civitai) : null;
const loraManagerArchiveResult = results.lora_manager_archive ? (Array.isArray(results.lora_manager_archive) ? results.lora_manager_archive[0] : results.lora_manager_archive) : null;
const hasResults = popular || modelListResult || hfResult || civitaiResult || loraManagerArchiveResult;
const knownDownloadRow = this.getDownloadSourceTableRow(missing, missing.download_source);
const hasResults = knownDownloadRow || popular || modelListResult || hfResult || civitaiResult || loraManagerArchiveResult;
const progressHtml = this.renderSearchProgress(state);
const hasActiveProgress = this.hasActiveSearchProgress(state);
@@ -5111,6 +5182,14 @@ class LinkerManagerDialog extends ComfyDialog {
}
const rows = [];
const rowKeys = new Set();
const addRow = (row) => {
if (!row) return;
const rowKey = row.downloadUrl || row.openUrl || `${row.sourceKey}:${row.model}:${row.filename}`;
if (rowKeys.has(rowKey)) return;
rowKeys.add(rowKey);
rows.push(row);
};
let statusHtml = '';
if (!hasActiveProgress && state?.lastAttemptError) {
@@ -5120,9 +5199,11 @@ class LinkerManagerDialog extends ComfyDialog {
statusHtml += this.renderStatusMessage(`No new matches found in ${searchedLabel}. Existing results are kept below.`, 'warning');
}
addRow(knownDownloadRow);
if (popular) {
const popularFilename = popular.filename || missing.original_path?.split('/').pop()?.split('\\').pop() || '';
rows.push({
addRow({
sourceKey: 'popular',
sourceLabel: 'Popular',
model: popular.name || popularFilename,
@@ -5138,7 +5219,7 @@ class LinkerManagerDialog extends ComfyDialog {
}
if (modelListResult && modelListResult.url) {
rows.push({
addRow({
sourceKey: 'model-list',
sourceLabel: 'Local Database',
model: modelListResult.name || modelListResult.filename,
@@ -5156,7 +5237,7 @@ class LinkerManagerDialog extends ComfyDialog {
if (hfResult && hfResult.url) {
const hfRepo = hfResult.repo_id || hfResult.repo || '';
const hfModelUrl = hfRepo ? `https://huggingface.co/${hfRepo}` : this.getModelCardUrl(hfResult.url);
rows.push({
addRow({
sourceKey: 'huggingface',
sourceLabel: 'HuggingFace',
model: hfRepo || hfResult.filename,
@@ -5173,7 +5254,7 @@ class LinkerManagerDialog extends ComfyDialog {
if (loraManagerArchiveResult && loraManagerArchiveResult.url) {
const archiveFilename = loraManagerArchiveResult.filename || missing.original_path?.split('/').pop()?.split('\\').pop() || '';
rows.push({
addRow({
sourceKey: 'lora-archive',
sourceLabel: 'LoRA Archive',
model: loraManagerArchiveResult.name || loraManagerArchiveResult.version_name || archiveFilename,
@@ -5192,7 +5273,7 @@ class LinkerManagerDialog extends ComfyDialog {
const modelUrl = civitaiResult.url || (civitaiResult.model_id ? `https://civitai.com/models/${civitaiResult.model_id}${civitaiResult.version_id ? `?modelVersionId=${civitaiResult.version_id}` : ''}` : '');
const downloadFilename = missing.civitai_info?.expected_filename || civitaiResult.filename || civitaiResult.name;
const modelName = missing.civitai_info?.version_name ? `${missing.civitai_info.model_name} v${missing.civitai_info.version_name}` : (civitaiResult.name || downloadFilename || 'Model');
rows.push({
addRow({
sourceKey: 'civitai',
sourceLabel: 'CivitAI',
model: modelName,
@@ -5210,16 +5291,7 @@ class LinkerManagerDialog extends ComfyDialog {
const html = `${progressHtml}${statusHtml}${this.renderSearchResultsTable(rows)}`;
container.innerHTML = html;
// Attach download listeners
const downloadBtns = container.querySelectorAll('.search-download-btn');
downloadBtns.forEach(btn => {
btn.addEventListener('click', () => {
const url = btn.dataset.url;
const filename = btn.dataset.filename;
const category = btn.dataset.category;
this.downloadFromSearch(missing, url, filename, category, btn);
});
});
this.wireSearchDownloadButtons(container, missing);
}
/**