refactor: centralize API helpers, extract shared UI utilities, simplify dropdown positioning
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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: []
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user