diff --git a/core/settings.py b/core/settings.py index 8117d0b..5256b48 100644 --- a/core/settings.py +++ b/core/settings.py @@ -33,6 +33,8 @@ SETTINGS_SCHEMA: List[Dict[str, Any]] = [ { "serverKey": "auto_open_on_missing", "localKey": "ModelResolver.autoOpenOnMissing", "type": "boolean", "default": False }, { "serverKey": "workflow_hash_metadata_enabled", "localKey": "ModelResolver.workflowHashMetadataEnabled", "type": "boolean", "default": True }, { "serverKey": "workflow_dependency_marker_enabled", "localKey": "ModelResolver.workflowDependencyMarkerEnabled", "type": "boolean", "default": False }, + { "serverKey": "node_toolbox_icon_enabled", "localKey": "ModelResolver.nodeToolboxIconEnabled", "type": "boolean", "default": True }, + { "serverKey": "node_context_menu_enabled", "localKey": "ModelResolver.nodeContextMenuEnabled", "type": "boolean", "default": True }, { "serverKey": "download_backend", "localKey": "ModelResolver.downloadBackend", "type": "backend", "default": "python" }, { "serverKey": "aria2c_path", "localKey": "ModelResolver.aria2cPath", "type": "string", "default": "" }, { "serverKey": "proxy_url", "localKey": "ModelResolver.proxyUrl", "type": "string", "default": "" }, @@ -193,6 +195,12 @@ def normalize_settings(settings: Optional[Mapping[str, Any]]) -> Dict[str, Any]: data["workflow_dependency_marker_enabled"] = bool_setting( data.get("workflow_dependency_marker_enabled"), False ) + data["node_toolbox_icon_enabled"] = bool_setting( + data.get("node_toolbox_icon_enabled"), True + ) + data["node_context_menu_enabled"] = bool_setting( + data.get("node_context_menu_enabled"), True + ) data["download_backend"] = normalize_download_backend(data.get("download_backend")) data["aria2c_path"] = str(data.get("aria2c_path") or "").strip() data["proxy_url"] = str(data.get("proxy_url") or "").strip() diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 4c58a9f..6b481c0 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -1415,6 +1415,38 @@ test('node context integration preserves existing menu hooks and refreshes after ]); }); +test('node context integration respects isNodeContextMenuEnabled when disabled', () => { + const configureNodeContextMenu = eval(`(${extractMethod(modelResolverSource, 'configureNodeContextMenu')})`); + class NodeType {} + NodeType.prototype.getExtraMenuOptions = function(_canvas, options) { + options.push({ content: 'Original action' }); + return options; + }; + + const model = { + node_id: 8, + widget_index: 0, + widget_name: 'ckpt_name', + original_path: 'model.safetensors', + full_path: 'E:\\models\\model.safetensors', + category: 'checkpoints', + exists: true, + }; + const resolver = { + isNodeContextMenuEnabled: () => false, + getResolvedModelsForNodeContextMenu: () => [model], + }; + + configureNodeContextMenu.call(resolver, NodeType); + const node = new NodeType(); + const options = []; + const result = node.getExtraMenuOptions(null, options); + + assert.equal(result, options); + assert.equal(options.length, 1); + assert.equal(options[0].content, 'Original action'); +}); + test('node context integration injects the menu when a node overrides getMenuOptions', () => { const configureNodeContextMenu = eval(`(${extractMethod(modelResolverSource, 'configureNodeContextMenu')})`); class NodeType {} diff --git a/tests/test_frontend_entrypoint.mjs b/tests/test_frontend_entrypoint.mjs index 28e22cd..6ef7b85 100644 --- a/tests/test_frontend_entrypoint.mjs +++ b/tests/test_frontend_entrypoint.mjs @@ -38,8 +38,15 @@ test('frontend entrypoint registers the ComfyUI extension contract', () => { /api\.fetchApi\(\s*["']\/model_resolver\/workflow-model-hashes["']/ ); assert.match(resolverSource, /id:\s*MODEL_RESOLVER_OPEN_COMMAND_ID/); + assert.match(resolverSource, /id:\s*MODEL_RESOLVER_NODE_TOOLBOX_COMMAND_ID/); assert.match(resolverSource, /function:\s*\(\)\s*=>\s*modelResolver\.activateResolverButton\(\)/); + assert.match(resolverSource, /function:\s*\(\)\s*=>\s*modelResolver\.handleNodeSelectionToolboxClick\(\)/); assert.match(resolverSource, /keybindings:\s*\[\s*MODEL_RESOLVER_OPEN_DEFAULT_KEYBINDING/); + assert.match(modelResolverSource, /isNodeWithModels\(node\)/); + assert.match(modelResolverSource, /handleNodeSelectionToolboxClick\(\)/); + assert.match(modelResolverSource, /isNodeToolboxIconEnabled\(\)/); + assert.match(modelResolverSource, /isNodeContextMenuEnabled\(\)/); + assert.match(resolverSource, /modelResolver\.isNodeToolboxIconEnabled\(\)/); const hookPatterns = { setup: /\bsetup\s*:/, @@ -47,6 +54,7 @@ test('frontend entrypoint registers the ComfyUI extension contract', () => { nodeCreated: /\bnodeCreated\s*\(/, loadedGraphNode: /\bloadedGraphNode\s*\(/, afterConfigureGraph: /\bafterConfigureGraph\s*\(/, + getSelectionToolboxCommands: /\bgetSelectionToolboxCommands\s*\(/, }; for (const [hook, pattern] of Object.entries(hookPatterns)) { assert.match( diff --git a/tests/test_settings.py b/tests/test_settings.py index a2f481b..47e3204 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -27,6 +27,22 @@ class SettingsTests(unittest.TestCase): ] ) + def test_node_toolbox_icon_defaults_to_enabled(self): + self.assertTrue(normalize_settings({})["node_toolbox_icon_enabled"]) + self.assertFalse( + normalize_settings({"node_toolbox_icon_enabled": "false"})[ + "node_toolbox_icon_enabled" + ] + ) + + def test_node_context_menu_defaults_to_enabled(self): + self.assertTrue(normalize_settings({})["node_context_menu_enabled"]) + self.assertFalse( + normalize_settings({"node_context_menu_enabled": "false"})[ + "node_context_menu_enabled" + ] + ) + def test_auto_open_on_missing_defaults_to_disabled(self): self.assertFalse(normalize_settings({})["auto_open_on_missing"]) self.assertTrue( diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index d967f41..3a04052 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -5371,7 +5371,7 @@ button.mr-info-link:disabled { .mr-image-prompt-grid { display: grid; grid-template-columns: minmax(0, 1fr) 18px minmax(0, 1fr); - align-items: center; + align-items: stretch; gap: var(--mr-spacing-10); } @@ -5380,6 +5380,9 @@ button.mr-info-link:disabled { flex-direction: column; gap: var(--mr-spacing-6); min-width: 0; + min-height: 0; + height: 100%; + box-sizing: border-box; } .mr-image-prompt-side.is-context-menu { @@ -5387,9 +5390,13 @@ button.mr-info-link:disabled { } .mr-image-prompt-side-label { + display: inline-flex; + align-items: center; + gap: var(--mr-spacing-4); color: var(--mr-text-muted); font-size: var(--mr-font-xs); font-weight: 650; + line-height: 1; } .mr-image-prompt-select { @@ -5453,6 +5460,7 @@ button.mr-info-link:disabled { } .mr-image-prompt-arrow { + align-self: center; color: var(--mr-accent); font-size: var(--mr-font-lg); font-weight: 700; @@ -5460,8 +5468,15 @@ button.mr-info-link:disabled { } .mr-image-prompt-empty { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + box-sizing: border-box; + flex: 1 1 auto; + min-height: 80px; margin: 0; - padding: var(--mr-spacing-8); + padding: var(--mr-spacing-10) var(--mr-spacing-8); border: 1px dashed var(--mr-border); border-radius: var(--mr-radius-md, var(--mr-radius-lg)); color: var(--mr-text-muted); @@ -6182,8 +6197,34 @@ button.mr-info-link:disabled { } .mr-image-transfer-change-label { + display: inline-flex; + align-items: center; + gap: var(--mr-spacing-4); color: var(--mr-text-muted); font-size: var(--mr-font-xs); + line-height: 1; +} + +.mr-image-transfer-label-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + line-height: 1; +} + +.mr-image-transfer-label-icon svg { + width: 12px; + height: 12px; + display: block; +} + +.mr-image-transfer-label-icon-current { + color: var(--mr-color-success, #72d38a); +} + +.mr-image-transfer-label-icon-imported { + color: var(--mr-color-primary, #5af); } .mr-image-transfer-change-arrow { diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css index 08a47ad..40b6be7 100644 --- a/web/css/resolver-shell.css +++ b/web/css/resolver-shell.css @@ -1181,6 +1181,9 @@ top: var(--mr-spacing-10); right: var(--mr-spacing-10); z-index: var(--mr-z-floating-button); + display: inline-flex; + align-items: center; + gap: var(--mr-spacing-8); padding: var(--mr-spacing-8) var(--mr-spacing-16); color: var(--mr-text, #f3f3f3); font-size: var(--mr-font-lg); @@ -1190,15 +1193,152 @@ box-shadow: var(--mr-shadow-floating); } +.model-resolver-floating-button .mr-floating-btn-icon { + width: 18px; + height: 18px; + flex-shrink: 0; +} + .model-resolver-floating-button:hover { background: var(--mr-accent, #72d38a); transform: scale(1.05); } +.mr-sidebar-tab-icon, +[data-testid="comfyui-model-resolver-tab-button"] i, +[data-testid*="model-resolver"] i, +.comfyui-model-resolver-tab-button i, .comfyui-model-resolver-tab-button .mdi, -.comfyui-model-resolver-tab-button .mdi-link-variant { - font-size: var(--mr-font-2xl); +.comfyui-model-resolver-tab-button .mdi-link-variant, +.comfyui-model-resolver-tab-button .mr-sidebar-tab-icon, +#model-resolver-button i, +#model-resolver-button .mdi-link-variant, +#model-resolver-button .link-variant, +#model-resolver-button .mr-topbar-tab-icon, +.mr-node-toolbar-icon { + display: inline-block; + width: 20px; + height: 20px; + font-size: 20px; line-height: 1; + vertical-align: middle; + background-color: currentColor; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain; +} + +.mr-sidebar-tab-icon::before, +[data-testid="comfyui-model-resolver-tab-button"] i::before, +[data-testid*="model-resolver"] i::before, +.comfyui-model-resolver-tab-button i::before, +.comfyui-model-resolver-tab-button .mdi::before, +.comfyui-model-resolver-tab-button .mdi-link-variant::before, +.comfyui-model-resolver-tab-button .mr-sidebar-tab-icon::before, +#model-resolver-button i::before, +#model-resolver-button .mdi-link-variant::before, +#model-resolver-button .link-variant::before, +#model-resolver-button .mr-topbar-tab-icon::before, +.mr-node-toolbar-icon::before { + content: none !important; + display: none !important; +} + +.mr-model-resolver-node-menu::before { + content: "" !important; + display: inline-block !important; + width: 14px !important; + height: 14px !important; + vertical-align: -2px !important; + margin-right: 6px !important; + background-color: currentColor !important; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; +} + +.comfyui-selection-toolbox .mr-node-toolbar-icon { + width: 18px; + height: 18px; + font-size: 18px; +} + +.comfyui-selection-toolbox button:hover .mr-node-toolbar-icon, +button:has(.mr-node-toolbar-icon):hover .mr-node-toolbar-icon { + color: var(--mr-accent, #72d38a); +} + +.litecontextmenu.mr-node-selection-context-menu { + z-index: 10000 !important; +} + +.mr-node-selection-context-menu .litemenu-title { + padding-left: 26px !important; + position: relative !important; + font-weight: 600 !important; +} + +.mr-node-selection-context-menu .litemenu-title::before { + content: '' !important; + position: absolute !important; + left: 7px !important; + top: 50% !important; + transform: translateY(-50%) !important; + width: 14px !important; + height: 14px !important; + background-color: var(--mr-accent, #72d38a) !important; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-item, +.litegraph .litemenu-entry.mr-model-resolver-node-menu { + padding-left: 26px !important; + position: relative !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-item::before, +.litegraph .litemenu-entry.mr-model-resolver-node-menu::before { + content: '' !important; + position: absolute !important; + left: 7px !important; + top: 50% !important; + transform: translateY(-50%) !important; + width: 14px !important; + height: 14px !important; + background-color: currentColor !important; + opacity: 0.85 !important; + pointer-events: none !important; + display: inline-block !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-item:hover::before, +.litegraph .litemenu-entry.mr-model-resolver-node-menu:hover::before { + opacity: 1 !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-show-in-resolver::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat center / contain !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-show-info::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E") no-repeat center / contain !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-open-folder::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E") no-repeat center / contain !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-open-resolver::before, +.litegraph .litemenu-entry.mr-model-resolver-node-menu::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; +} + +.litegraph .litemenu-entry.mr-node-ctx-model::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z'/%3E%3Cpath d='m3.3 7 8.7 5 8.7-5'/%3E%3Cpath d='M12 22V12'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z'/%3E%3Cpath d='m3.3 7 8.7 5 8.7-5'/%3E%3Cpath d='M12 22V12'/%3E%3C/svg%3E") no-repeat center / contain !important; } .mr-sidebar-dock-panel { diff --git a/web/resolver.js b/web/resolver.js index 457d57e..165f52f 100644 --- a/web/resolver.js +++ b/web/resolver.js @@ -6,6 +6,7 @@ import { app } from "../../../scripts/app.js"; import { + MODEL_RESOLVER_NODE_TOOLBOX_COMMAND_ID, MODEL_RESOLVER_OPEN_COMMAND_ID, MODEL_RESOLVER_OPEN_DEFAULT_KEYBINDING, ModelResolver as ModelResolverClass, @@ -24,7 +25,20 @@ app.registerExtension({ label: "Open Model Resolver", function: () => modelResolver.activateResolverButton(), }, + { + id: MODEL_RESOLVER_NODE_TOOLBOX_COMMAND_ID, + label: "Model Resolver", + icon: "mr-node-toolbar-icon", + function: () => modelResolver.handleNodeSelectionToolboxClick(), + }, ], + getSelectionToolboxCommands(item) { + if (modelResolver.isNodeToolboxIconEnabled() && modelResolver.isNodeWithModels(item)) { + modelResolver.lastSelectionToolboxNode = item; + return [MODEL_RESOLVER_NODE_TOOLBOX_COMMAND_ID]; + } + return []; + }, keybindings: [ MODEL_RESOLVER_OPEN_DEFAULT_KEYBINDING, ], diff --git a/web/resolver/model_resolver.js b/web/resolver/model_resolver.js index 3242331..d808839 100644 --- a/web/resolver/model_resolver.js +++ b/web/resolver/model_resolver.js @@ -8,6 +8,7 @@ import { isSidebarButtonActive } from "./utils/dom_patch_utils.js"; import { fetchJson as fetchJsonRequest } from "./utils/api_client.js"; import { ResolverManagerDialog } from "./resolver_dialog.js"; import { showNotification } from "../utils/notification_utils.js"; +import { getSvgIcon } from "../utils/icon_utils.js"; import { safeStorage } from "./utils/html_utils.js"; import { serializeKeybindingCombo, @@ -17,6 +18,8 @@ import { import { buildModelResolverNodeMenu, getImmediateModelsForNode, + getResolvedModelFilename, + getResolvedModelMenuLabel, getResolvedModelsForNode, isExistingResolvedModel, matchesWorkflowModelReference, @@ -31,6 +34,39 @@ import { const log = createModuleLogger('model_resolver'); export const MODEL_RESOLVER_OPEN_COMMAND_ID = "ModelResolver.OpenModelResolver"; +export const MODEL_RESOLVER_NODE_TOOLBOX_COMMAND_ID = "ModelResolver.NodeActions"; + +const MODEL_REFERENCE_WIDGET_NAMES = new Set([ + "lora", + "lora_name", + "ckpt_name", + "checkpoint", + "checkpoint_name", + "vae_name", + "clip_name", + "clip_name1", + "clip_name2", + "clip_name3", + "clip_name4", + "control_net_name", + "cnet", + "model_name", + "model_path", + "unet_name", + "gguf_name", + "upscale_model_name", + "style_model_name", + "gligen_name", + "audio_encoder_name", + "bg_removal_name", + "photomaker_model_name", + "sam_model_name", + "text_encoder", + "existing_lora", + "hypernetwork_name", +]); + +const MODEL_FILE_EXTENSION_PATTERN = /\.(ckpt|pt2?|bin|pth|safetensors|pkl|sft|onnx|gguf)(?:$|[?#])/i; export const MODEL_RESOLVER_OPEN_DEFAULT_KEYBINDING = Object.freeze({ commandId: MODEL_RESOLVER_OPEN_COMMAND_ID, combo: Object.freeze({ key: "|", ctrl: true, shift: true }), @@ -79,6 +115,127 @@ function applyStoredFrontendLoggingPreference() { } } +export function ensureSidebarTabIconStyle() { + if (typeof document === "undefined" || document.getElementById("mr-sidebar-tab-icon-style")) return; + const style = document.createElement("style"); + style.id = "mr-sidebar-tab-icon-style"; + style.textContent = ` + .mr-sidebar-tab-icon, + [data-testid="comfyui-model-resolver-tab-button"] i, + [data-testid*="model-resolver"] i, + .comfyui-model-resolver-tab-button i, + #model-resolver-button .mr-topbar-tab-icon, + .mr-node-toolbar-icon { + display: inline-block !important; + width: 20px !important; + height: 20px !important; + font-size: 20px !important; + line-height: 1 !important; + vertical-align: middle !important; + background-color: currentColor !important; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + + .mr-sidebar-tab-icon::before, + [data-testid="comfyui-model-resolver-tab-button"] i::before, + [data-testid*="model-resolver"] i::before, + .comfyui-model-resolver-tab-button i::before, + #model-resolver-button i::before, + .mr-node-toolbar-icon::before { + content: none !important; + display: none !important; + } + + .comfyui-selection-toolbox .mr-node-toolbar-icon { + width: 18px !important; + height: 18px !important; + font-size: 18px !important; + } + + .comfyui-selection-toolbox button:hover .mr-node-toolbar-icon, + button:has(.mr-node-toolbar-icon):hover .mr-node-toolbar-icon { + color: var(--mr-accent, #72d38a) !important; + } + + .litecontextmenu.mr-node-selection-context-menu { + z-index: 10000 !important; + } + + .mr-node-selection-context-menu .litemenu-title { + padding-left: 26px !important; + position: relative !important; + font-weight: 600 !important; + } + + .mr-node-selection-context-menu .litemenu-title::before { + content: '' !important; + position: absolute !important; + left: 7px !important; + top: 50% !important; + transform: translateY(-50%) !important; + width: 14px !important; + height: 14px !important; + background-color: var(--mr-accent, #72d38a) !important; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-item, + .litegraph .litemenu-entry.mr-model-resolver-node-menu { + padding-left: 26px !important; + position: relative !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-item::before, + .litegraph .litemenu-entry.mr-model-resolver-node-menu::before { + content: '' !important; + position: absolute !important; + left: 7px !important; + top: 50% !important; + transform: translateY(-50%) !important; + width: 14px !important; + height: 14px !important; + background-color: currentColor !important; + opacity: 0.85 !important; + pointer-events: none !important; + display: inline-block !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-item:hover::before, + .litegraph .litemenu-entry.mr-model-resolver-node-menu:hover::before { + opacity: 1 !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-show-in-resolver::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-show-info::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-open-folder::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-open-resolver::before, + .litegraph .litemenu-entry.mr-model-resolver-node-menu::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1.85' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z' fill='%23fff' fill-opacity='0.16'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + + .litegraph .litemenu-entry.mr-node-ctx-model::before { + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z'/%3E%3Cpath d='m3.3 7 8.7 5 8.7-5'/%3E%3Cpath d='M12 22V12'/%3E%3C/svg%3E") no-repeat center / contain !important; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z'/%3E%3Cpath d='m3.3 7 8.7 5 8.7-5'/%3E%3Cpath d='M12 22V12'/%3E%3C/svg%3E") no-repeat center / contain !important; + } + `; + document.head.appendChild(style); +} + // Main extension class export class ModelResolver { constructor() { @@ -107,6 +264,7 @@ export class ModelResolver { this.customNodeModelAdapterStates = new WeakMap(); this.missingModelsPopupObserver = null; this.nativeMissingModelsPending = false; + this.lastSelectionToolboxNode = null; this.nativeMissingModelsAutoOpened = false; this.nativeMissingModelStorePromise = null; this.nativeMissingModelStore = null; @@ -120,6 +278,7 @@ export class ModelResolver { setup = async () => { window.ModelResolver = this; applyStoredFrontendLoggingPreference(); + ensureSidebarTabIconStyle(); loadStylesWhenNeeded(); // Remove any existing button @@ -158,9 +317,10 @@ export class ModelResolver { } try { + ensureSidebarTabIconStyle(); registerSidebarTab.call(app.extensionManager, { id: this.sidebarTabId, - icon: "mdi mdi-link-variant", + icon: "mr-sidebar-tab-icon", title: "Resolver", tooltip: this.openTooltip, type: "custom", @@ -282,16 +442,27 @@ export class ModelResolver { } this.openTooltipTargets = liveTargets; - document.querySelectorAll(`.${this.sidebarTabId}-tab-button`).forEach((button) => { + document.querySelectorAll(`[data-testid="${this.sidebarTabId}-tab-button"], .${this.sidebarTabId}-tab-button`).forEach((button) => { if (button instanceof HTMLElement) { this.applyTooltipToElement(button); + const icon = button.querySelector("i, span.mdi, span.icon, .side-bar-button-icon"); + if (icon && !icon.classList.contains("mr-sidebar-tab-icon")) { + icon.classList.add("mr-sidebar-tab-icon"); + } } }); } getSidebarButton() { - const button = document.querySelector(`.${this.sidebarTabId}-tab-button`); - return button instanceof HTMLElement ? button : null; + const button = document.querySelector(`[data-testid="${this.sidebarTabId}-tab-button"], .${this.sidebarTabId}-tab-button`); + if (button instanceof HTMLElement) { + const icon = button.querySelector("i, span.mdi, span.icon, .side-bar-button-icon"); + if (icon && !icon.classList.contains("mr-sidebar-tab-icon")) { + icon.classList.add("mr-sidebar-tab-icon"); + } + return button; + } + return null; } getVisibleResolverButton() { @@ -313,6 +484,237 @@ export class ModelResolver { this.handleResolverButtonClick(); } + isNodeWithModels(node) { + if (!node) return false; + + try { + const models = this.getResolvedModelsForNodeContextMenu(node); + if (Array.isArray(models) && models.length > 0) { + return true; + } + } catch (_) { + // ignore + } + + if (this.isCustomNodeModelNode(node)) { + return true; + } + + if (Array.isArray(node.widgets)) { + for (const widget of node.widgets) { + if (!widget) continue; + const name = String(widget.name || "").toLowerCase(); + if (MODEL_REFERENCE_WIDGET_NAMES.has(name)) { + return true; + } + const val = typeof widget.value === "string" ? widget.value.trim() : ""; + if (val && MODEL_FILE_EXTENSION_PATTERN.test(val)) { + return true; + } + if (this.isCustomNodeModelWidget?.(node, widget.name)) { + return true; + } + } + } + + return false; + } + + isCustomNodeModelNode(node) { + return Boolean(getCustomNodeModelAdapter(node)); + } + + getSelectedNode() { + const canvas = app.canvas; + if (canvas?.selected_nodes) { + const nodes = Object.values(canvas.selected_nodes); + if (nodes.length > 0) return nodes[0]; + } + if (canvas?.current_node) { + return canvas.current_node; + } + if (canvas?.selectedItems) { + for (const item of canvas.selectedItems) { + if (item && (item.comfyClass || item.type || item.widgets)) return item; + } + } + return this.lastSelectionToolboxNode || null; + } + + async handleNodeSelectionToolboxClick() { + const node = this.getSelectedNode(); + if (!node) { + this.activateResolverButton(); + return; + } + + let models = this.getResolvedModelsForNodeContextMenu(node); + if (!models || models.length === 0) { + const data = await this.ensureCurrentNodeContextAnalysis(); + if (data) { + models = this.getResolvedModelsForNodeContextMenu(node); + } + } + + if ((!models || models.length === 0) && Array.isArray(node.widgets)) { + for (const widget of node.widgets) { + if (!widget || typeof widget.value !== "string") continue; + const val = widget.value.trim(); + if (!val || ["none", "null", "undefined"].includes(val.toLowerCase())) continue; + const name = String(widget.name || "").toLowerCase(); + if (MODEL_REFERENCE_WIDGET_NAMES.has(name) || MODEL_FILE_EXTENSION_PATTERN.test(val)) { + models = [{ + name: val, + filename: getResolvedModelFilename({ original_path: val }), + original_path: val, + widget_name: widget.name, + exists: false, + resolution_pending: true, + }]; + break; + } + } + } + + const owner = this; + const handlers = { + showInResolver: async (model) => { + const resolvedModel = await owner.resolveNodeContextMenuModel(model); + if (resolvedModel) { + owner.showResolvedNodeModelInResolver(resolvedModel); + } else { + owner.openResolverManager(); + } + }, + showInfo: async (model) => { + const resolvedModel = await owner.resolveNodeContextMenuModel(model); + if (resolvedModel) { + if (!owner.dialog) { + owner.dialog = new ResolverManagerDialog(); + window.ModelResolverDialog = owner.dialog; + } + owner.dialog?.showModelInfo(toResolverContextModel(resolvedModel)); + } + }, + openContainingFolder: async (model) => { + const resolvedModel = await owner.resolveNodeContextMenuModel(model); + if (resolvedModel) { + if (!owner.dialog) { + owner.dialog = new ResolverManagerDialog(); + window.ModelResolverDialog = owner.dialog; + } + owner.dialog?.openContainingFolder(toResolverContextModel(resolvedModel)); + } + }, + }; + + const resolvedModels = (models || []).filter(model => ( + isExistingResolvedModel(model) || model.resolution_pending === true + )); + + let options; + if (resolvedModels.length === 1) { + const model = resolvedModels[0]; + options = [ + { + content: "Show in Model Resolver", + className: "mr-node-ctx-item mr-node-ctx-show-in-resolver", + callback: () => handlers.showInResolver(model), + }, + { + content: "Show info", + className: "mr-node-ctx-item mr-node-ctx-show-info", + callback: () => handlers.showInfo(model), + }, + { + content: "Open containing folder", + className: "mr-node-ctx-item mr-node-ctx-open-folder", + callback: () => handlers.openContainingFolder(model), + }, + null, + { + content: "Open Model Resolver", + className: "mr-node-ctx-item mr-node-ctx-open-resolver", + callback: () => owner.activateResolverButton(), + }, + ]; + } else if (resolvedModels.length > 1) { + options = resolvedModels.map(model => { + const label = getResolvedModelMenuLabel(model, c => owner.dialog?.getCategoryDisplayName?.(c)); + return { + title: label, + content: label, + className: "mr-node-ctx-item mr-node-ctx-model", + has_submenu: true, + submenu: { + options: [ + { + content: "Show in Model Resolver", + className: "mr-node-ctx-item mr-node-ctx-show-in-resolver", + callback: () => handlers.showInResolver(model), + }, + { + content: "Show info", + className: "mr-node-ctx-item mr-node-ctx-show-info", + callback: () => handlers.showInfo(model), + }, + { + content: "Open containing folder", + className: "mr-node-ctx-item mr-node-ctx-open-folder", + callback: () => handlers.openContainingFolder(model), + }, + ], + }, + }; + }); + options.push(null); + options.push({ + content: "Open Model Resolver", + className: "mr-node-ctx-item mr-node-ctx-open-resolver", + callback: () => owner.activateResolverButton(), + }); + } else { + options = [ + { + content: "Open Model Resolver", + className: "mr-node-ctx-item mr-node-ctx-open-resolver", + callback: () => owner.activateResolverButton(), + }, + ]; + } + + const event = window.event; + let x = event?.clientX; + let y = event?.clientY; + + if (!x || !y) { + const btn = document.querySelector(".mr-node-toolbar-icon")?.closest("button") || document.activeElement; + if (btn?.getBoundingClientRect) { + const rect = btn.getBoundingClientRect(); + x = Math.round(rect.left); + y = Math.round(rect.bottom + 4); + } else if (node?.pos) { + x = window.innerWidth / 2; + y = window.innerHeight / 2; + } + } + + const ContextMenuClass = window.LiteGraph?.ContextMenu || (typeof LiteGraph !== "undefined" ? LiteGraph.ContextMenu : null); + if (ContextMenuClass) { + const menuEvent = (event && event.clientX) ? event : { clientX: x, clientY: y, pageX: x, pageY: y }; + new ContextMenuClass(options, { + event: menuEvent, + left: x, + top: y, + title: "Model Resolver", + className: "dark mr-node-selection-context-menu", + scale: 1, + }, window); + } else { + this.activateResolverButton(); + } + } + getNodeContextWorkflowState() { const workflow = app?.graph?.serialize?.(); if (!workflow) return { workflow: null, signature: null }; @@ -532,6 +934,7 @@ export class ModelResolver { const originalGetExtraMenuOptions = target.getExtraMenuOptions; const originalGetMenuOptions = target.getMenuOptions; const addModelResolverMenu = (node, targetOptions) => { + if (typeof owner.isNodeContextMenuEnabled === 'function' && !owner.isNodeContextMenuEnabled()) return; if (!Array.isArray(targetOptions)) return; if (targetOptions.some(option => ( String(option?.className || '').includes('mr-model-resolver-node-menu') @@ -791,7 +1194,7 @@ export class ModelResolver { handleSidebarButtonClick(event) { const target = event.target instanceof Element ? event.target : null; - const button = target?.closest(`.${this.sidebarTabId}-tab-button`); + const button = target?.closest(`[data-testid="${this.sidebarTabId}-tab-button"], .${this.sidebarTabId}-tab-button`); if (!(button instanceof HTMLElement)) return; if (!this.dialog?.isVisible()) return; if (this.dialog.pendingDockToSidebar) return; @@ -855,7 +1258,7 @@ export class ModelResolver { // Create button group with Model Resolver button const ModelResolverButton = new ComfyButton({ - icon: "link-variant", + icon: "link-variant mr-topbar-tab-icon", action: this.handleResolverButtonClick, content: "Model Resolver", classList: "comfyui-button comfyui-menu-mobile-collapse" @@ -1026,6 +1429,15 @@ export class ModelResolver { return safeStorage.getItem('ModelResolver.workflowDependencyMarkerEnabled') === 'true'; } + isNodeToolboxIconEnabled() { + return safeStorage.getItem('ModelResolver.nodeToolboxIconEnabled') !== 'false'; + } + + isNodeContextMenuEnabled() { + return safeStorage.getItem('ModelResolver.nodeContextMenuEnabled') !== 'false'; + } + + setupWorkflowHashMetadataInjection() { window.__ModelResolverWorkflowHashMetadataOwner = this; if (window.__ModelResolverWorkflowHashMetadataPatched) { @@ -2080,7 +2492,7 @@ export class ModelResolver { // Create a floating button as fallback for legacy ComfyUI versions this.resolverButton = $el("button", { id: this.buttonId, - textContent: "🔗 Model Resolver", + innerHTML: `${getSvgIcon("modelResolver", "currentColor", "mr-floating-btn-icon")}Model Resolver`, onclick: this.handleResolverButtonClick, className: "model-resolver-floating-button" }); diff --git a/web/resolver/node_context_menu.js b/web/resolver/node_context_menu.js index 9796529..05683f5 100644 --- a/web/resolver/node_context_menu.js +++ b/web/resolver/node_context_menu.js @@ -4,7 +4,7 @@ function getResolvedModelPath(model = {}) { return String(model.full_path || model.path || model.resolved_path || '').trim(); } -function getResolvedModelFilename(model = {}) { +export function getResolvedModelFilename(model = {}) { const source = String( model.name || model.filename @@ -24,7 +24,7 @@ function getResolvedModelCategoryLabel(model = {}, formatCategory = null) { return String(formatted || category).toUpperCase(); } -function getResolvedModelMenuLabel(model = {}, formatCategory = null) { +export function getResolvedModelMenuLabel(model = {}, formatCategory = null) { const category = getResolvedModelCategoryLabel(model, formatCategory); const widgetName = String(model.widget_name || '').trim(); const filename = getResolvedModelFilename(model); @@ -58,14 +58,17 @@ function createModelActions(model, handlers = {}) { return [ { content: 'Show in Model Resolver', + className: 'mr-node-ctx-item mr-node-ctx-show-in-resolver', callback: () => handlers.showInResolver?.(model), }, { content: 'Show info', + className: 'mr-node-ctx-item mr-node-ctx-show-info', callback: () => handlers.showInfo?.(model), }, { content: 'Open containing folder', + className: 'mr-node-ctx-item mr-node-ctx-open-folder', callback: () => handlers.openContainingFolder?.(model), }, ]; @@ -298,6 +301,7 @@ export function buildModelResolverNodeMenu(models = [], handlers = {}, options = ? createModelActions(resolvedModels[0], handlers) : resolvedModels.map(model => ({ title: getResolvedModelMenuLabel(model, options.formatCategory), + className: 'mr-node-ctx-item mr-node-ctx-model', has_submenu: true, submenu: { options: createModelActions(model, handlers), diff --git a/web/resolver/shell/dialog_shell_methods.js b/web/resolver/shell/dialog_shell_methods.js index 8fe3b82..86a9f99 100644 --- a/web/resolver/shell/dialog_shell_methods.js +++ b/web/resolver/shell/dialog_shell_methods.js @@ -622,7 +622,7 @@ export const dialogShellMethods = { requestAnimationFrame(() => { if (!this.pendingDockToSidebar || this.docked) return; - const button = document.querySelector(`.${this.sidebarTabId}-tab-button`); + const button = document.querySelector(`[data-testid="${this.sidebarTabId}-tab-button"], .${this.sidebarTabId}-tab-button`); if (button instanceof HTMLElement) { const isActive = isSidebarButtonActive(button); @@ -727,7 +727,7 @@ export const dialogShellMethods = { requestAnimationFrame(() => { if (dockContainer && !dockContainer.isConnected) return; - const button = document.querySelector(`.${this.sidebarTabId}-tab-button`); + const button = document.querySelector(`[data-testid="${this.sidebarTabId}-tab-button"], .${this.sidebarTabId}-tab-button`); if (!(button instanceof HTMLElement)) return; const isActive = isSidebarButtonActive(button); diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index 4f35539..c5993a9 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -475,6 +475,24 @@ export const optionsMethods = { + +
@@ -967,6 +985,8 @@ export const optionsMethods = { const autoOpenOnMissingInput = this.contentElement.querySelector('#mr-options-auto-open-on-missing'); const workflowHashMetadataInput = this.contentElement.querySelector('#mr-options-workflow-hash-metadata'); const workflowDependencyMarkerInput = this.contentElement.querySelector('#mr-options-workflow-dependency-marker'); + const nodeToolboxIconInput = this.contentElement.querySelector('#mr-options-node-toolbox-icon'); + const nodeContextMenuInput = this.contentElement.querySelector('#mr-options-node-context-menu'); const addDependencyNodeBtn = this.contentElement.querySelector('#mr-options-add-dependency-node'); const downloadBackendInput = this.contentElement.querySelector('#mr-options-download-backend'); const aria2cPathInput = this.contentElement.querySelector('#mr-options-aria2c-path'); @@ -1098,6 +1118,8 @@ export const optionsMethods = { autoOpenOnMissingInput, workflowHashMetadataInput, workflowDependencyMarkerInput, + nodeToolboxIconInput, + nodeContextMenuInput, downloadBackendInput, aria2cPathInput, aria2AutoStopInput, @@ -3554,6 +3576,8 @@ export const optionsMethods = { auto_open_on_missing: Boolean(autoOpenOnMissingInput?.checked), workflow_hash_metadata_enabled: Boolean(workflowHashMetadataInput?.checked), workflow_dependency_marker_enabled: Boolean(workflowDependencyMarkerInput?.checked), + node_toolbox_icon_enabled: Boolean(nodeToolboxIconInput?.checked), + node_context_menu_enabled: Boolean(nodeContextMenuInput?.checked), download_backend: downloadBackend, aria2c_path: aria2cPathInput?.value?.trim() || '', aria2_auto_stop_daemon: Boolean(aria2AutoStopInput?.checked), diff --git a/web/resolver/views/tabs_image_metadata_methods.js b/web/resolver/views/tabs_image_metadata_methods.js index a1c88b9..71d64ff 100644 --- a/web/resolver/views/tabs_image_metadata_methods.js +++ b/web/resolver/views/tabs_image_metadata_methods.js @@ -1371,7 +1371,8 @@ export const imageMetadataMethods = {
- Current text field + + Current text field ${this.getMetadataTooltipBadge(targetRoleHint, `Current ${row.role} prompt role information`)} ${targetHtml} @@ -1379,7 +1380,8 @@ export const imageMetadataMethods = {
- Imported + + Imported ${this.getMetadataTooltipBadge(sourceRoleHint, `Imported ${row.role} prompt role information`)} ${sourceHtml} @@ -2123,12 +2125,18 @@ export const imageMetadataMethods = { return `
- Current + + + Current + ${this.escapeHtml(currentLabel)}
- Imported + + + Imported + ${importedValueHtml}
@@ -2260,12 +2268,18 @@ export const imageMetadataMethods = { return `
- Current${this.escapeHtml(slotLabel)} + + + Current${this.escapeHtml(slotLabel)} + ${this.escapeHtml(currentLabel)}
- Imported + + + Imported + Select node
diff --git a/web/utils/icon_utils.js b/web/utils/icon_utils.js index 1419c79..1e2f68c 100644 --- a/web/utils/icon_utils.js +++ b/web/utils/icon_utils.js @@ -68,6 +68,10 @@ const SVG_ICONS = { windowRestore: ``, x: ``, civitai: ``, + workflow: ``, + 'import': ``, + circleDot: ``, + modelResolver: ``, huggingface: `` };