diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 0042d5f..2f671fc 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -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; diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index b155aa0..3d47a60 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -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 = `
Refreshing local matches for ${this.escapeHtml(displayName)}...
`; + } + + 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; diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index ba8ca42..6574b72 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -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 += `
`; - html += `
Local Matches
`; + const localRefreshId = `local-matches-refresh-${missing.node_id}-${missing.widget_index}`; + html += `
`; + html += `Local Matches`; + html += ``; + html += `
`; html += `
`; html += this.renderLocalMatchesContent(missing, missingIndex); html += `
`;