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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user