Add manual refresh button for local model matches in missing browser

When a model is missing, users can now click a refresh button in the
'Local Matches' header to rescan local folders and refresh matches
for that specific model. The button requests POST /model_resolver/local-matches
with force_rescan=true, shows loading state, and updates the UI on
completion or error. CSS was added for the new button element.
This commit is contained in:
Dariusz L
2026-06-22 02:00:40 +02:00
parent 590b4a4969
commit 74a696a4bc
3 changed files with 129 additions and 1 deletions
+30
View File
@@ -1418,6 +1418,33 @@
font-size: 10px;
letter-spacing: 0.08em;
}
.mr-local-matches-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mr-local-matches-header > span {
min-width: 0;
}
.mr-local-matches-refresh-btn,
#model-resolver-modal .mr-local-matches-refresh-btn {
width: 26px;
min-width: 26px;
max-width: 26px;
height: 24px;
min-height: 24px;
padding: 0;
flex: 0 0 26px;
font-size: 0;
line-height: 1;
}
.mr-local-matches-refresh-btn .mr-combo-refresh-icon,
#model-resolver-modal .mr-local-matches-refresh-btn .mr-combo-refresh-icon {
width: 13px;
height: 13px;
display: block;
}
.mr-missing-browser .mr-muted-note,
.mr-missing-browser .mr-no-matches {
padding: 9px 10px;
@@ -5746,6 +5773,7 @@
}
#model-resolver-modal .mr-missing-detail-pane .mr-local-link-btn,
#model-resolver-modal .mr-missing-detail-pane .mr-combo-refresh-btn,
#model-resolver-modal .mr-missing-detail-pane .mr-local-matches-refresh-btn,
#model-resolver-modal .mr-missing-detail-pane .mr-search-result-action-btn,
#model-resolver-modal .mr-missing-detail-pane .mr-download-section .mr-search-button-picker > .mr-btn {
background: var(--mr-missing-control-bg, rgba(255,255,255,0.035));
@@ -5757,6 +5785,7 @@
}
#model-resolver-modal .mr-missing-detail-pane .mr-local-link-btn:hover:not(:disabled),
#model-resolver-modal .mr-missing-detail-pane .mr-combo-refresh-btn:hover:not(:disabled),
#model-resolver-modal .mr-missing-detail-pane .mr-local-matches-refresh-btn:hover:not(:disabled),
#model-resolver-modal .mr-missing-detail-pane .mr-search-result-action-btn:hover:not(:disabled),
#model-resolver-modal .mr-missing-detail-pane .mr-download-section .mr-search-button-picker > .mr-btn:hover:not(:disabled) {
background: var(--mr-missing-control-bg-hover, rgba(255,255,255,0.06));
@@ -5766,6 +5795,7 @@
}
#model-resolver-modal .mr-missing-detail-pane .mr-local-link-btn:focus-visible,
#model-resolver-modal .mr-missing-detail-pane .mr-combo-refresh-btn:focus-visible,
#model-resolver-modal .mr-missing-detail-pane .mr-local-matches-refresh-btn:focus-visible,
#model-resolver-modal .mr-missing-detail-pane .mr-search-result-action-btn:focus-visible,
#model-resolver-modal .mr-missing-detail-pane .mr-download-section .mr-search-button-picker > .mr-btn:focus-visible {
outline: none;
@@ -938,6 +938,92 @@ export const resolveDownloadMethods = {
this.updateBatchFooterButtons?.();
},
getLocalMatchRefreshTarget(missing = {}) {
return missing.civitai_info?.expected_filename
|| missing.expected_filename
|| missing.download_source?.filename
|| missing.original_path
|| missing.name
|| '';
},
async refreshLocalMatchesForMissing(missing, { button = null } = {}) {
if (!missing || button?.disabled) return [];
const targetFilename = String(this.getLocalMatchRefreshTarget(missing) || '');
if (!targetFilename) {
this.showNotification('No model filename available for local refresh.', 'warning');
return [];
}
const minRefreshFeedback = new Promise(resolve => setTimeout(resolve, 420));
const refreshAnimation = this.startRefreshButtonAnimation?.(button);
const body = this.contentElement?.querySelector(`#local-matches-body-${missing.node_id}-${missing.widget_index}`);
const displayName = targetFilename.split('/').pop()?.split('\\').pop() || targetFilename;
const missingKey = this.getMissingModelKey(missing);
const currentMissing = (this.missingModels || []).find(item => this.getMissingModelKey(item) === missingKey) || missing;
const previousMatches = this.cloneLocalMatches?.(currentMissing.matches || missing.matches || []) || [];
try {
if (button) {
button.disabled = true;
button.classList.add('mr-btn-is-disabled', 'mr-is-refreshing');
}
if (body) {
body.innerHTML = `<div class="mr-no-matches">Refreshing local matches for ${this.escapeHtml(displayName)}...</div>`;
}
const response = await api.fetchApi('/model_resolver/local-matches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: targetFilename,
category: missing.category || '',
force_rescan: true
})
});
if (!response.ok) {
throw new Error(`Local match refresh failed: ${response.status}`);
}
const data = await response.json();
const matches = Array.isArray(data.matches) ? data.matches : [];
currentMissing.matches = matches;
missing.matches = matches;
if (currentMissing.is_urn) {
currentMissing.__urnLocalMatchesFilename = targetFilename;
missing.__urnLocalMatchesFilename = targetFilename;
}
this.rememberDownloadedLocalMatchesForMissing(currentMissing, matches, {
targetFilename,
category: missing.category || '',
manualRefresh: true
});
this.allModels = null;
this.invalidateLoadedModelsCacheForActiveWorkflow?.();
await minRefreshFeedback;
this.refreshLocalMatchesUiForMissing(currentMissing);
return matches;
} catch (error) {
await minRefreshFeedback;
console.error('Model Resolver: local match refresh failed:', error);
currentMissing.matches = previousMatches;
missing.matches = previousMatches;
this.refreshLocalMatchesUiForMissing(currentMissing);
this.showNotification(`Local match refresh failed: ${error.message}`, 'error');
return [];
} finally {
refreshAnimation?.cancel?.();
if (button) {
button.disabled = false;
button.classList.remove('mr-btn-is-disabled', 'mr-is-refreshing');
}
}
},
refreshMissingListRowForMissing(missing) {
if (!missing || !this.contentElement) return;
+13 -1
View File
@@ -676,6 +676,14 @@ export const missingBrowserMethods = {
this.wireDownloadSearchPanel(container, missing);
this.updateSelectedBarForMissing(missing);
const localRefreshId = `local-matches-refresh-${missing.node_id}-${missing.widget_index}`;
const localRefreshBtn = container.querySelector(`#${localRefreshId}`);
if (localRefreshBtn) {
localRefreshBtn.addEventListener('click', () => this.refreshLocalMatchesForMissing?.(missing, {
button: localRefreshBtn
}));
}
const locateId = `locate-${missing.node_id}-${missing.widget_index}`;
const locateBtn = container.querySelector(`#${locateId}`);
const locateTarget = this.getMissingLocateTarget(missing);
@@ -1110,7 +1118,11 @@ export const missingBrowserMethods = {
// LEFT COLUMN: Local Matches
html += `<div class="mr-column">`;
html += `<div class="mr-column-header">Local Matches</div>`;
const localRefreshId = `local-matches-refresh-${missing.node_id}-${missing.widget_index}`;
html += `<div class="mr-column-header mr-local-matches-header">`;
html += `<span>Local Matches</span>`;
html += `<button id="${localRefreshId}" type="button" aria-label="Refresh local matches" data-tooltip="Rescan local model folders and refresh matches for this model" class="mr-btn mr-btn-secondary mr-btn-sm mr-btn-icon-only mr-local-matches-refresh-btn"><span class="mr-refresh-spin-target">${getSvgIcon('refreshCw', 'currentColor', 'mr-combo-refresh-icon')}</span></button>`;
html += `</div>`;
html += `<div id="local-matches-body-${missing.node_id}-${missing.widget_index}">`;
html += this.renderLocalMatchesContent(missing, missingIndex);
html += `</div>`;