From 2fe2092dfc746fc3ac1212d6af3b347cbce18a68 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 25 Jun 2026 00:59:06 +0200 Subject: [PATCH] refactor: centralize API helpers, extract shared UI utilities, simplify dropdown positioning --- web/resolver/resolver_dialog.js | 27 +++ .../search/download_target_methods.js | 176 +++--------------- web/resolver/search/search_panel.js | 11 +- web/resolver/utils/render_format_methods.js | 11 ++ web/resolver/views/missing_browser_methods.js | 15 +- web/resolver/views/model_info_methods.js | 33 +--- 6 files changed, 79 insertions(+), 194 deletions(-) diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index 7b80f30..3eb419c 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -1,4 +1,5 @@ import { $el, ComfyDialog } from "../../../../scripts/ui.js"; +import { api } from "../../../../scripts/api.js"; import { dialogShellMethods } from "./shell/dialog_shell_methods.js"; import { lifecycleGraphMethods } from "./shell/lifecycle_graph_methods.js"; import { workflowStateMethods } from "./shell/workflow_state_methods.js"; @@ -209,6 +210,32 @@ export class ResolverManagerDialog extends ComfyDialog { window.addEventListener('focus', this._boundSyncSearchProgressAfterResume); document.addEventListener('visibilitychange', this._boundSyncSearchProgressAfterResume); } + + async fetchJson(endpoint, options = {}, errorContext = 'API Request') { + try { + const fetchOptions = { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}) + } + }; + const response = await api.fetchApi(endpoint, fetchOptions); + if (!response.ok) { + throw new Error(`Server returned ${response.status}: ${response.statusText}`); + } + if (response.status === 204) { + return null; + } + return await response.json(); + } catch (error) { + console.error(`Model Resolver: ${errorContext} failed:`, error); + if (typeof this.showNotification === 'function') { + this.showNotification(error.message || 'API request failed', 'error'); + } + throw error; + } + } } function applyDialogMethods(...sources) { diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index f630354..760f9b5 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -776,23 +776,19 @@ export const downloadTargetMethods = { }, {}); }, - normalizeBaseModelMappingKey(value = '') { - return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); - }, - resolveBaseModelPathMapping(baseModel = '', mappings = this.getBaseModelPathMappings()) { const text = String(baseModel || ''); if (Object.prototype.hasOwnProperty.call(mappings, text)) { return mappings[text]; } - const token = this.normalizeBaseModelMappingKey(text); + const token = this.normalizeBaseModelToken(text); if (!token) return text; const normalizedEntries = Object.entries(mappings) .map(([key, value]) => ({ key, - token: this.normalizeBaseModelMappingKey(key), + token: this.normalizeBaseModelToken(key), value })) .filter(entry => entry.token); @@ -1676,161 +1672,43 @@ export const downloadTargetMethods = { window.addEventListener('mousedown', handleOutsidePointer, true); }, - portalDropdownList(listEl) { - if (!listEl) return; - if (listEl.id) { - document.querySelectorAll('.mr-download-target-list[data-ml-floating-portal="true"]').forEach(existing => { - if (existing !== listEl && existing.id === listEl.id) { - existing.remove(); - } - }); - } - if (listEl.dataset.mlFloatingPortal !== 'true') { - listEl.dataset.mlFloatingPortal = 'true'; - document.body.appendChild(listEl); - } - listEl.classList.add('mr-download-target-floating'); - }, - - cleanupFloatingDropdownPositioning(listEl) { - if (typeof listEl?._mlFloatingPositionCleanup === 'function') { - listEl._mlFloatingPositionCleanup(); - listEl._mlFloatingPositionCleanup = null; - } - }, - hideDropdownList(listEl) { if (!listEl) return; - this.cleanupFloatingDropdownPositioning(listEl); - listEl.style.display = 'none'; - }, - - positionFloatingDropdownList(listEl, anchorEl) { - if (!listEl || !anchorEl || listEl.style.display === 'none') return; - if (listEl.classList.contains('mr-download-folder-browser')) return; - - const rect = anchorEl.getBoundingClientRect(); - const viewportPadding = 12; - const viewportHeight = Math.max(240, window.innerHeight || document.documentElement.clientHeight || 0); - if (rect.bottom < viewportPadding || rect.top > viewportHeight - viewportPadding) { - this.hideDropdownList(listEl); - return; + if (listEl._mlTreePicker) { + listEl._mlTreePicker.hide(); + } else { + listEl.style.display = 'none'; } - - const gap = 4; - const spaceBelow = viewportHeight - rect.bottom - viewportPadding - gap; - const spaceAbove = rect.top - viewportPadding - gap; - const openAbove = spaceBelow < 160 && spaceAbove > spaceBelow; - const availableHeight = Math.max(80, openAbove ? spaceAbove : spaceBelow); - - listEl.style.position = 'fixed'; - listEl.style.top = '0px'; - listEl.style.maxHeight = `${availableHeight}px`; - this.fitDropdownListWidth(listEl, anchorEl); - - const popupHeight = Math.min(listEl.offsetHeight || availableHeight, availableHeight); - const rawTop = openAbove - ? rect.top - popupHeight - gap - : rect.bottom + gap; - const maxTop = Math.max(viewportPadding, viewportHeight - viewportPadding - popupHeight); - const top = Math.min(Math.max(rawTop, viewportPadding), maxTop); - listEl.style.top = `${Math.round(top)}px`; - }, - - bindFloatingDropdownPositioning(listEl, anchorEl) { - if (!listEl || !anchorEl || listEl.classList.contains('mr-download-folder-browser')) return; - this.cleanupFloatingDropdownPositioning(listEl); - - const updatePosition = (event) => { - if (event?.type === 'scroll' && event.target instanceof Node && listEl.contains(event.target)) { - return; - } - this.positionFloatingDropdownList(listEl, anchorEl); - }; - window.addEventListener('resize', updatePosition, true); - window.addEventListener('scroll', updatePosition, true); - listEl._mlFloatingPositionCleanup = () => { - window.removeEventListener('resize', updatePosition, true); - window.removeEventListener('scroll', updatePosition, true); - }; }, showDropdownList(listEl, anchorEl = null) { if (!listEl) return; const anchor = anchorEl || listEl.previousElementSibling - || listEl.parentElement?.querySelector?.('.mr-download-target-input'); + || listEl.parentElement?.querySelector?.('.mr-download-target-input') + || listEl.parentElement?.querySelector?.('.mr-download-target-select'); if (!anchor) { listEl.style.display = 'block'; return; } - this.portalDropdownList(listEl); - listEl.style.display = 'block'; - this.positionFloatingDropdownList(listEl, anchor); - this.bindFloatingDropdownPositioning(listEl, anchor); - requestAnimationFrame(() => this.positionFloatingDropdownList(listEl, anchor)); - }, - - fitDropdownListWidth(listEl, anchorEl = null) { - if (!listEl || listEl.classList.contains('mr-download-folder-browser')) return; - - const anchor = anchorEl - || listEl.previousElementSibling - || listEl.parentElement?.querySelector?.('.mr-download-target-input'); - if (!anchor) return; - - const anchorRect = anchor.getBoundingClientRect(); - const inputWidth = Math.ceil(anchorRect.width || 0); - if (!inputWidth) return; - - const viewportPadding = 12; - const viewportWidth = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 0); - const maxByInput = inputWidth * 2; - const maxByViewport = Math.max(inputWidth, viewportWidth - viewportPadding * 2); - const absoluteMaxWidth = Math.max(inputWidth, Math.min(maxByInput, maxByViewport)); - - listEl.style.minWidth = `${inputWidth}px`; - listEl.style.width = `${inputWidth}px`; - listEl.style.maxWidth = `${absoluteMaxWidth}px`; - - const listStyle = window.getComputedStyle(listEl); - const listChromeWidth = [ - listStyle.borderLeftWidth, - listStyle.borderRightWidth, - listStyle.paddingLeft, - listStyle.paddingRight - ].reduce((width, value) => width + (Number.parseFloat(value) || 0), 0); - const optionWidths = Array.from(listEl.querySelectorAll('.mr-download-target-option')).map(option => ( - Math.max(option.scrollWidth, option.getBoundingClientRect().width || 0) - )); - const contentWidth = Math.ceil(Math.max(inputWidth, ...optionWidths) + listChromeWidth + 2); - let targetWidth = Math.max(inputWidth, Math.min(contentWidth, absoluteMaxWidth)); - const rightSpace = Math.max(inputWidth, viewportWidth - viewportPadding - anchorRect.left); - const leftSpace = Math.max(inputWidth, anchorRect.right - viewportPadding); - - if (targetWidth > rightSpace && leftSpace > rightSpace) { - targetWidth = Math.min(targetWidth, leftSpace); - if (listEl.classList.contains('mr-download-target-floating')) { - listEl.style.left = `${Math.round(Math.max(viewportPadding, anchorRect.right - targetWidth))}px`; - listEl.style.right = 'auto'; - } else { - listEl.style.left = 'auto'; - listEl.style.right = '0'; - } - } else { - targetWidth = Math.min(targetWidth, rightSpace); - if (listEl.classList.contains('mr-download-target-floating')) { - listEl.style.left = `${Math.round(Math.max(viewportPadding, anchorRect.left))}px`; - listEl.style.right = 'auto'; - } else { - listEl.style.left = '0'; - listEl.style.right = 'auto'; - } + if (listEl._mlTreePicker) { + listEl._mlTreePicker.cleanup(); } - listEl.style.width = `${Math.round(targetWidth)}px`; - listEl.style.maxWidth = `${Math.round(targetWidth)}px`; + listEl._mlTreePicker = createFloatingTreePicker({ + listEl, + anchorEl: anchor, + duplicateSelector: '.mr-download-target-list[data-ml-floating-portal="true"], .mr-combo-list[data-ml-floating-portal="true"]', + floatingClass: 'mr-download-target-floating', + viewportPadding: 12, + gap: 4, + setMinWidth: true, + minPopupWidth: 0, + maxPopupWidth: 560 + }); + + listEl._mlTreePicker.show(); }, enableWheelScrollChaining(scrollEl) { @@ -1975,11 +1853,7 @@ export const downloadTargetMethods = { return this.downloadFolderBrowserExpandedGroups.get(key); }; - const getBaseDirectoryLabel = (baseDirectory = '') => { - const clean = String(baseDirectory || '').replace(/[\\\/]+$/, ''); - if (!clean) return 'Default root'; - return clean.split(/[\\\/]+/).filter(Boolean).pop() || clean; - }; + const getFolderBrowserContextAttrs = (category = '', subfolder = '', baseDirectory = '', tooltip = 'Right-click to open this folder') => { const context = this.getDownloadTargetFolderContext(category, subfolder, baseDirectory); if (!context) return ''; @@ -2115,7 +1989,7 @@ export const downloadTargetMethods = { rootGroupsMap.set(key, { key, baseDirectory, - label: getBaseDirectoryLabel(baseDirectory), + label: this.getBaseDirectoryLabel(baseDirectory), entries: [] }); } diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 627a744..a742e76 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -1481,11 +1481,6 @@ export const searchPanelMethods = { }, getSearchResultsTableLayout(rows = []) { - const textWidth = (value, charPx = 6, minPx = 40, maxPx = 180) => { - const length = String(value || '').length; - return Math.max(minPx, Math.min(maxPx, Math.ceil(length * charPx))); - }; - const maxSourceLabel = rows.reduce( (max, row) => Math.max(max, String(row.sourceLabel || row.sourceKey || '').length), 'Source'.length @@ -1503,9 +1498,9 @@ export const searchPanelMethods = { return Math.max(max, count); }, 1); - const sourcePx = Math.max(118, textWidth('x'.repeat(maxSourceLabel), 6, 34, 100) + 52); - const matchPx = textWidth('x'.repeat(maxMatchLabel), 7, 34, 76) + 22; - const sizePx = textWidth('x'.repeat(maxSizeLabel), 6.5, 42, 88) + 30; + const sourcePx = Math.max(118, this.estimateTextWidth('x'.repeat(maxSourceLabel), 6, 34, 100) + 52); + const matchPx = this.estimateTextWidth('x'.repeat(maxMatchLabel), 7, 34, 76) + 22; + const sizePx = this.estimateTextWidth('x'.repeat(maxSizeLabel), 6.5, 42, 88) + 30; const actionsPx = Math.max(96, (maxActions * 28) + (Math.max(0, maxActions - 1) * 8) + 26); const modelMinPx = 210; const tableMinPx = Math.ceil(sourcePx + matchPx + sizePx + actionsPx + modelMinPx); diff --git a/web/resolver/utils/render_format_methods.js b/web/resolver/utils/render_format_methods.js index 7f2ff55..b98d5e6 100644 --- a/web/resolver/utils/render_format_methods.js +++ b/web/resolver/utils/render_format_methods.js @@ -310,5 +310,16 @@ export const renderFormatMethods = { } return parts.join(' | '); + }, + + getBaseDirectoryLabel(baseDirectory = '') { + const clean = String(baseDirectory || '').replace(/[\\\/]+$/, ''); + if (!clean) return 'Default root'; + return clean.split(/[\\\/]+/).filter(Boolean).pop() || clean; + }, + + estimateTextWidth(value, charPx = 6, minPx = 40, maxPx = 180) { + const length = String(value || '').length; + return Math.max(minPx, Math.min(maxPx, Math.ceil(length * charPx))); } }; diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index a223e70..06f91ef 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -202,7 +202,6 @@ export const missingBrowserMethods = { getMissingModelsListLayout(missingModels = []) { const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); - const textWidth = (value, charPx) => Math.ceil(String(value || '').length * charPx); let modelPx = 180; let typePx = 66; @@ -212,10 +211,10 @@ export const missingBrowserMethods = { const typeLabel = missing.category ? this.getCategoryDisplayName(missing.category) : 'unknown'; modelPx = Math.max( modelPx, - textWidth(filename, 7.2) + 12, - textWidth(nodeLabel, 5.5) + 34 + this.estimateTextWidth(filename, 7.2, 0, 1000) + 12, + this.estimateTextWidth(nodeLabel, 5.5, 0, 1000) + 34 ); - typePx = Math.max(typePx, textWidth(typeLabel, 5.8) + 18); + typePx = Math.max(typePx, this.estimateTextWidth(typeLabel, 5.8, 0, 1000) + 18); } return { @@ -826,11 +825,7 @@ export const missingBrowserMethods = { }); return Array.from(byIdentity.values()); }; - const getBaseDirectoryLabel = (baseDirectory = '') => { - const clean = String(baseDirectory || '').replace(/[\\\/]+$/, ''); - if (!clean) return 'Default root'; - return clean.split(/[\\\/]+/).filter(Boolean).pop() || clean; - }; + const joinLocalModelPath = (basePath = '', relativePath = '') => { if (typeof this.joinLocalPath === 'function') { return this.joinLocalPath(basePath, relativePath); @@ -897,7 +892,7 @@ export const missingBrowserMethods = { filename, folderPath, baseDirectory, - baseLabel: getBaseDirectoryLabel(baseDirectory), + baseLabel: this.getBaseDirectoryLabel(baseDirectory), fullPath, isPreferred: category === preferredCategory, searchText: [ diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index da1a3ff..424e978 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -1,4 +1,4 @@ -import { app } from "../../../../../scripts/app.js"; +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"; @@ -142,18 +142,12 @@ export const modelInfoMethods = { } try { - const response = await api.fetchApi('/model_resolver/open-containing-folder', { + await this.fetchJson('/model_resolver/open-containing-folder', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) - }); - - if (!response.ok) { - throw new Error(`Open folder failed: ${response.status}`); - } + }, 'Open containing folder'); } catch (error) { - console.error('Model Resolver: Open folder error:', error); - this.showNotification('Failed to open containing folder', 'error'); + // Already logged and notified inside fetchJson } }, @@ -174,32 +168,21 @@ export const modelInfoMethods = { try { // Search CivitAI for this model using hash (pass resolved_path for hash lookup) - const response = await api.fetchApi('/model_resolver/civitai-search', { + const data = await this.fetchJson('/model_resolver/civitai-search', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: name, category: model.category, resolved_path: model.resolved_path || '' }) - }); + }, 'Search CivitAI'); - if (!response.ok) { - this.showNotification('Nie znaleziono modelu na CivitAI', 'error'); - return; - } - - const data = await response.json(); - if (data.url) { + if (data && data.url) { window.open(data.url, '_blank'); } else { - // Try direct search on CivitAI - const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); - const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`; - window.open(searchUrl, '_blank'); + throw new Error('No URL in search result'); } } catch (e) { - console.error('Model Resolver: Error searching CivitAI:', e); // Fall back to direct search const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`;