- Guard workflow-tab-click handler with setTimeout and generation ownership check to avoid stale state updates - Add generation-based refresh cancellation so outdated retry attempts are discarded - Expand workflow signature to detect more model reference types, normalize widget values, and include subgraph definitions
202 lines
9.0 KiB
JavaScript
202 lines
9.0 KiB
JavaScript
import { $el, ComfyDialog } from "../../../../scripts/ui.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 { downloadTargetMethods } from "./search/download_target_methods.js";
|
|
import { searchPanelMethods } from "./search/search_panel.js";
|
|
import { queueMethods } from "./actions/queue_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 { 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.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.activeDownloads = {}; // Track active downloads
|
|
this.downloadProgressByMissingKey = new Map(); // missing model key -> last known download UI state
|
|
this.queuePanelActiveTab = 'queued';
|
|
this.searchResultCache = new Map();
|
|
this.backgroundSearchJobs = new Map();
|
|
this.searchProgressTimers = new Map();
|
|
this.urnResolvePromises = new Map();
|
|
this.urnLocalMatchPromises = new Map();
|
|
this.cachedAnalysisData = null;
|
|
this.cachedWorkflowSignature = null;
|
|
this.cachedLoadedModelsData = null;
|
|
this.cachedLoadedModelsSignature = null;
|
|
this.selectedMissingModelKey = 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.dockButton = null;
|
|
this.undockButton = null;
|
|
this._floatingRectBeforeDock = null;
|
|
this._dragging = false;
|
|
this._dragStart = null;
|
|
this._analysisProgressToken = null;
|
|
this._workflowDataLoadToken = null;
|
|
this._loadedModelsLoadToken = null;
|
|
this._locateAnimationFrame = null;
|
|
this._viewportClampFrame = null;
|
|
this.activeWorkflowRouteKey = this.getActiveWorkflowRouteKey();
|
|
this.activeWorkflowSignature = null;
|
|
this._workflowRefreshTimer = null;
|
|
this._workflowRefreshRetryTimer = null;
|
|
this._workflowRefreshGeneration = 0;
|
|
this._workflowRefreshExpectedRoute = null;
|
|
this._workflowRefreshPreviousSignature = null;
|
|
this._boundHandleViewportResize = () => this.scheduleModalViewportClamp(true);
|
|
|
|
// Create backdrop overlay for click-outside-to-close
|
|
this.backdrop = $el("div.model-resolver-backdrop", {
|
|
parent: document.body
|
|
});
|
|
|
|
// 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-divider.mr-context-menu-divider-source", {
|
|
"data-menu-divider": "source"
|
|
}),
|
|
$el("div.mr-context-menu-item.mr-context-menu-action-civitai", {
|
|
"data-menu-action": "civitai",
|
|
onclick: () => this.handleContextMenuAction('civitai')
|
|
}, [
|
|
$el("span.mr-context-menu-item-icon", {
|
|
innerHTML: getSvgIcon('civitai', 'currentColor', 'mr-context-menu-item-svg')
|
|
}),
|
|
$el("span", { textContent: "Open in CivitAI" })
|
|
]),
|
|
$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-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.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);
|
|
}
|
|
}
|
|
|
|
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(
|
|
searchPanelMethods,
|
|
modelInfoMethods,
|
|
renderFormatMethods,
|
|
workflowStateMethods,
|
|
selectionMethods,
|
|
dialogShellMethods,
|
|
downloadTargetMethods,
|
|
optionsMethods,
|
|
tabsLoadedMethods,
|
|
queueMethods,
|
|
lifecycleGraphMethods,
|
|
missingBrowserMethods,
|
|
resolveDownloadMethods,
|
|
workflowUpdateMethods
|
|
);
|