Adds a downloads panel inside the resolve queue UI with two tabs: - Active: live downloads - History: completed or already-downloaded entries History is persisted in localStorage (model_resolver_download_history, capped at 200 entries) via new helper methods on queueMethods. Downloads complete by calling rememberCompletedDownloadHistory(), which records filename, category, workflow, node, target path, size, status and timestamp. CSS additions cover the subtab bar, history item layout, and a small status badge.
220 lines
10 KiB
JavaScript
220 lines
10 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.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.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.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-workflow", {
|
|
"data-menu-divider": "workflow"
|
|
}),
|
|
$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('internalLink', '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-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
|
|
);
|