377 lines
18 KiB
JavaScript
377 lines
18 KiB
JavaScript
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
|
import { api } from "../../../../scripts/api.js";
|
|
import { safeStorage } from "./utils/html_utils.js";
|
|
import { showNotification as showNotificationUtils } from "../utils/notification_utils.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";
|
|
import { workflowUpdateMethods } from "./shell/workflow_update_methods.js";
|
|
import { modelInfoMethods } from "./views/model_info_methods.js";
|
|
import { missingBrowserMethods } from "./views/missing_browser_methods.js";
|
|
import { optionsMethods } from "./views/options_methods.js";
|
|
import { tabsLoadedMethods } from "./views/tabs_loaded_methods.js";
|
|
import { imageMetadataMethods } from "./views/tabs_image_metadata_methods.js";
|
|
import { downloadTargetMethods } from "./search/download_target_methods.js";
|
|
import { searchPanelMethods } from "./search/search_panel.js";
|
|
import { searchSourceMethods } from "./search/search_source_methods.js";
|
|
import { baseModelAliasMethods } from "./search/base_model_alias_methods.js";
|
|
import { searchHashMethods } from "./search/search_hash_methods.js";
|
|
import { searchStateMethods } from "./search/search_state_methods.js";
|
|
import { missingModelStateMethods } from "./views/missing_model_state_methods.js";
|
|
import { modelHashCompareMethods } from "./views/model_hash_compare_methods.js";
|
|
import { workflowIdentityMethods } from "./shell/workflow_identity_methods.js";
|
|
import { queueMethods } from "./actions/queue_methods.js";
|
|
import { queueStorageMethods } from "./actions/queue_storage_methods.js";
|
|
import { downloadProgressMethods } from "./actions/download_progress_methods.js";
|
|
import { resolveDownloadMethods } from "./actions/resolve_download_methods.js";
|
|
import { selectionMethods } from "./actions/selection_methods.js";
|
|
import { renderFormatMethods } from "./utils/render_format_methods.js";
|
|
import { fetchJson as fetchJsonRequest } from "./utils/api_client.js";
|
|
import { getSvgIcon } from "../utils/icon_utils.js";
|
|
export class ResolverManagerDialog extends ComfyDialog {
|
|
constructor() {
|
|
super();
|
|
this.currentWorkflow = null;
|
|
this.missingModels = [];
|
|
this.allModels = null; // list of all available models for dropdown
|
|
this.downloadDirectories = null;
|
|
this.downloadRootDirectories = null;
|
|
this.capabilities = null;
|
|
this.baseModels = null;
|
|
this.downloadSubfolders = new Map();
|
|
this.downloadTargetSelections = new Map(); // missing model key -> user/suggested download target
|
|
this.pendingResolutions = [];
|
|
this.pendingIndex = new Map(); // key -> index in pendingResolutions
|
|
this.workflowPendingSelections = new Map(); // workflow key -> queued selections
|
|
this.workflowAnalysisCaches = new Map(); // workflow key -> analyzed missing models
|
|
this.workflowLoadedModelCaches = new Map(); // workflow key -> loaded model inspector data
|
|
this.workflowSearchResultCaches = new Map(); // workflow key -> search results by missing model
|
|
this.workflowDownloadTargetSelectionCaches = new Map(); // workflow key -> suggested/user download folders
|
|
this.activeDownloads = {}; // Track active downloads
|
|
this.downloadHistoryStorageKey = 'model_resolver_download_history';
|
|
this.activeDownloadsStorageKey = 'model_resolver_active_downloads';
|
|
this.downloadHistoryLimit = 200;
|
|
this.downloadHistory = [];
|
|
this.downloadProgressByMissingKey = new Map(); // missing model key -> last known download UI state
|
|
this.queuePanelActiveTab = 'queued';
|
|
this.queueDownloadsActiveTab = 'active';
|
|
this.searchResultCache = new Map();
|
|
this.backgroundSearchJobs = new Map();
|
|
this.searchProgressTimers = new Map();
|
|
this.backendSearchProgressTimers = new Map();
|
|
this.urnResolvePromises = new Map();
|
|
this.urnLocalMatchPromises = new Map();
|
|
this.localMatchRefreshTokens = new Map();
|
|
this.searchUiRefreshFrames = new Map();
|
|
this.cachedAnalysisData = null;
|
|
this.cachedWorkflowSignature = null;
|
|
this.cachedLoadedModelsData = null;
|
|
this.cachedLoadedModelsSignature = null;
|
|
this.selectedMissingModelKey = null;
|
|
this.pendingWorkflowModelSelection = null;
|
|
this.batchSelectedMissingKeys = new Set();
|
|
this.lastBatchSelectedMissingKey = null;
|
|
this.activeFooterMenu = null;
|
|
this.batchSearchRunning = false;
|
|
this.batchSearchCancelRequested = false;
|
|
this.forceBatchSearch = false;
|
|
this.boundHandleOutsideClick = this.handleOutsideClick.bind(this);
|
|
this.activeTabStorageKey = 'model_resolver_active_tab';
|
|
this.activeTab = this.restoreActiveTab(); // Default tab
|
|
this.fullscreen = false;
|
|
this.returnToDockedAfterFullscreen = false;
|
|
this.docked = false;
|
|
this.dockContainer = null;
|
|
this.lastDockContainer = null;
|
|
this.pendingDockToSidebar = false;
|
|
this.sidebarTabId = "comfyui-model-resolver";
|
|
this.sidebarOpenModeStorageKey = "model_resolver_sidebar_open_mode";
|
|
this.missingBrowserSplitStorageKey = "model_resolver_missing_browser_detail_w";
|
|
this._missingBrowserExternalResizeFrame = null;
|
|
this._pendingMissingBrowserExternalResizeBrowser = null;
|
|
this._pendingMissingBrowserExternalResizeWidth = null;
|
|
this._missingBrowserObservedHostWidth = null;
|
|
this.showResolvedModelsStorageKey = "model_resolver_show_resolved_models";
|
|
this.showAutoDownloadModelsStorageKey = "model_resolver_show_auto_download_models";
|
|
this.showInactiveModelsStorageKey = "model_resolver_show_inactive_models";
|
|
this.localMatchAlternativesCollapsedStorageKey = "model_resolver_local_match_alternatives_collapsed";
|
|
this.showResolvedModels = safeStorage.getItem(this.showResolvedModelsStorageKey) === '1';
|
|
this.missingModelsTypeFilter = 'all';
|
|
this.missingModelsTypeFilterMenuOpen = false;
|
|
const storedShowAutoDownload = safeStorage.getItem(this.showAutoDownloadModelsStorageKey);
|
|
this.showAutoDownloadModels = storedShowAutoDownload === null
|
|
? true
|
|
: storedShowAutoDownload === '1';
|
|
this.showInactiveModels = safeStorage.getItem(this.showInactiveModelsStorageKey) === '1';
|
|
this.localMatchAlternativesCollapsed = safeStorage.getItem(this.localMatchAlternativesCollapsedStorageKey) === '1';
|
|
this.dockButton = null;
|
|
this.undockButton = null;
|
|
this._floatingRectBeforeDock = null;
|
|
this._modalResizeHandles = [];
|
|
this._dragging = false;
|
|
this._dragStart = null;
|
|
this._dragBounds = null;
|
|
this._dragPendingPosition = null;
|
|
this._dragAnimationFrame = null;
|
|
this._dragLayerCleanupTimer = null;
|
|
this._dragDockCandidate = false;
|
|
this._pendingDragDockRect = null;
|
|
this._lastDockContainerWidth = null;
|
|
this._dockedDragStart = null;
|
|
this._dockedDragPendingRect = null;
|
|
this._dockedDragAnimationFrame = null;
|
|
this._dragUndockCandidate = false;
|
|
this._sidebarSplitterPointerDownHandler = null;
|
|
this._sidebarSplitterMouseDownHandler = null;
|
|
this._sidebarSplitterDragState = null;
|
|
this._analysisProgressToken = null;
|
|
this._workflowAnalysisRequest = null;
|
|
this._workflowDataLoadToken = null;
|
|
this._loadedModelsLoadToken = null;
|
|
this._loadedModelsProgressToken = null;
|
|
this._locateAnimationFrame = null;
|
|
this._viewportClampFrame = null;
|
|
this.activeWorkflowRouteKey = this.getActiveWorkflowRouteKey();
|
|
this.activeWorkflowSignature = null;
|
|
this.activeMissingWorkflowSignature = null;
|
|
this._workflowRefreshTimer = null;
|
|
this._workflowRefreshRetryTimer = null;
|
|
this._workflowRefreshGeneration = 0;
|
|
this._workflowRefreshExpectedRoute = null;
|
|
this._workflowRefreshPreviousSignature = null;
|
|
this._workflowRefreshSuppressionDepth = 0;
|
|
this._comfyModelCatalogRefreshPromise = null;
|
|
this._contextMenuSourceLookupCache = new Map();
|
|
this._contextMenuSourceLookupToken = 0;
|
|
this._boundHandleViewportResize = () => this.scheduleModalViewportClamp(true);
|
|
|
|
// Create backdrop overlay for click-outside-to-close
|
|
this.backdrop = $el("div.model-resolver-backdrop", {
|
|
parent: document.body
|
|
});
|
|
|
|
this.dockDropPreview = $el("div.mr-dock-drop-preview", {
|
|
parent: document.body,
|
|
ariaHidden: "true",
|
|
innerHTML: `
|
|
<span class="mr-dock-drop-preview-icon">${getSvgIcon('dock')}</span>
|
|
<strong>Dock to sidebar</strong>
|
|
<span>Release to dock Model Resolver</span>
|
|
`
|
|
});
|
|
|
|
this.undockDropPreview = $el("div.mr-undock-drop-preview", {
|
|
parent: document.body,
|
|
ariaHidden: "true",
|
|
innerHTML: `
|
|
<span class="mr-dock-drop-preview-icon">${getSvgIcon('undock')}</span>
|
|
<strong>Undock to floating window</strong>
|
|
<span>Release to undock Model Resolver</span>
|
|
`
|
|
});
|
|
|
|
// Create context menu for model chips
|
|
this.contextMenu = $el("div.mr-context-menu", {
|
|
parent: document.body
|
|
}, [
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-show-info", {
|
|
"data-menu-action": "showInfo",
|
|
onclick: () => this.handleContextMenuAction('showInfo')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('eye', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Show Info" })
|
|
]),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-show-more", {
|
|
"data-menu-action": "showMore",
|
|
onclick: () => this.handleContextMenuAction('showMore')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('database', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Show More" })
|
|
]),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-suggest-subfolder", {
|
|
"data-menu-action": "suggestSubfolder",
|
|
onclick: () => this.handleContextMenuAction('suggestSubfolder')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('lightbulb', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Suggest Subfolder" })
|
|
]),
|
|
$el("div.mr-context-menu-divider.mr-context-menu-divider-source", {
|
|
"data-menu-divider": "source"
|
|
}),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-source", {
|
|
"data-menu-action": "source",
|
|
onclick: () => this.handleContextMenuAction('source')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('externalLink', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Open Source" })
|
|
]),
|
|
$el("div.mr-context-menu-divider.mr-context-menu-divider-workflow", {
|
|
"data-menu-divider": "workflow"
|
|
}),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-locate-node", {
|
|
"data-menu-action": "locateNode",
|
|
onclick: () => this.handleContextMenuAction('locateNode')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('locate', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Locate Node" })
|
|
]),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-switch-workflow", {
|
|
"data-menu-action": "switchWorkflow",
|
|
onclick: () => this.handleContextMenuAction('switchWorkflow')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('workflowBack', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Switch to Workflow" })
|
|
]),
|
|
$el("div.mr-context-menu-divider.mr-context-menu-divider-folder", {
|
|
"data-menu-divider": "folder"
|
|
}),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-compare-hashes", {
|
|
"data-menu-action": "compareHashes",
|
|
onclick: () => this.handleContextMenuAction('compareHashes')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('hash', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Compare Hashes" })
|
|
]),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-open-folder", {
|
|
"data-menu-action": "openFolder",
|
|
onclick: () => this.handleContextMenuAction('openFolder')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('folderOpen', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Open Containing Folder" })
|
|
])
|
|
]);
|
|
|
|
this.tooltipElement = $el("div.mr-global-tooltip", { parent: document.body });
|
|
|
|
// Selected model for context menu
|
|
this._contextMenuModel = null;
|
|
|
|
// Create dialog element using $el
|
|
this.element = $el("div.comfy-modal.model-resolver-modal", {
|
|
id: "model-resolver-modal",
|
|
parent: document.body
|
|
}, [
|
|
this.createHeader(),
|
|
this.createContent(),
|
|
this.createFooter()
|
|
]);
|
|
this.installModalResizeHandle();
|
|
|
|
this.tooltipObserver = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
this.bindTooltips(node);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.tooltipObserver.observe(this.element, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
this.bindTooltips(this.element);
|
|
|
|
// Add click listener to hide context menu when clicking outside
|
|
this.boundHandleContextMenuClick = (e) => this.handleContextMenuOutsideClick(e);
|
|
document.addEventListener('click', this.boundHandleContextMenuClick);
|
|
this.boundHandleFooterMenuClick = (e) => this.handleFooterMenuOutsideClick(e);
|
|
document.addEventListener('click', this.boundHandleFooterMenuClick);
|
|
window.addEventListener('resize', this._boundHandleViewportResize);
|
|
this._boundSyncSearchProgressAfterResume = () => {
|
|
if (document.visibilityState === 'hidden') return;
|
|
window.setTimeout(() => this.syncSearchProgressAfterResume?.(), 0);
|
|
};
|
|
window.addEventListener('focus', this._boundSyncSearchProgressAfterResume);
|
|
document.addEventListener('visibilitychange', this._boundSyncSearchProgressAfterResume);
|
|
}
|
|
|
|
async fetchJson(endpoint, options = {}, errorContext = 'API Request') {
|
|
return fetchJsonRequest(endpoint, options, errorContext, {
|
|
apiClient: api,
|
|
notify: typeof this.showNotification === 'function'
|
|
? this.showNotification.bind(this)
|
|
: null,
|
|
});
|
|
}
|
|
|
|
showNotification(message, type = 'success', options = {}) {
|
|
const duration = options?.duration || (type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000));
|
|
return showNotificationUtils(message, type, {
|
|
...options,
|
|
duration,
|
|
deduplicate: options?.deduplicate || false
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mixin Architecture Pattern:
|
|
*
|
|
* To prevent the dialog class from growing to an unmanageable size (e.g. 5,000+ lines),
|
|
* the dialog is split into multiple view/behavior-specific mixin files under `views/`, `actions/`, etc.
|
|
*
|
|
* Each mixin file exports a plain object containing methods that expect `this` to point to the
|
|
* active `ResolverManagerDialog` instance (accessing properties like `this.contentElement`, `this.activeTab`).
|
|
*
|
|
* Rationale:
|
|
* - Keeps UI views isolated in separate files.
|
|
* - Resolves circular dependency concerns when views trigger global dialog actions.
|
|
*
|
|
* Future Refactoring / Migration Path:
|
|
* - If UI complexity grows, transition these view objects to decoupled Web Components or independent ES6
|
|
* Tab classes extending a base component, passing the dialog controller instance as a constructor argument.
|
|
*/
|
|
function applyDialogMethods(...sources) {
|
|
for (const source of sources) {
|
|
for (const key of Reflect.ownKeys(source)) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
descriptor.enumerable = false;
|
|
Object.defineProperty(ResolverManagerDialog.prototype, key, descriptor);
|
|
}
|
|
}
|
|
}
|
|
|
|
applyDialogMethods(
|
|
searchSourceMethods,
|
|
baseModelAliasMethods,
|
|
searchHashMethods,
|
|
searchStateMethods,
|
|
missingModelStateMethods,
|
|
modelHashCompareMethods,
|
|
workflowIdentityMethods,
|
|
searchPanelMethods,
|
|
modelInfoMethods,
|
|
renderFormatMethods,
|
|
workflowStateMethods,
|
|
selectionMethods,
|
|
dialogShellMethods,
|
|
downloadTargetMethods,
|
|
optionsMethods,
|
|
tabsLoadedMethods,
|
|
imageMetadataMethods,
|
|
queueMethods,
|
|
queueStorageMethods,
|
|
downloadProgressMethods,
|
|
lifecycleGraphMethods,
|
|
missingBrowserMethods,
|
|
resolveDownloadMethods,
|
|
workflowUpdateMethods
|
|
);
|