Add node toolbox icon, context menu SVG icons, and toggle settings
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+142
-2
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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")}<span>Model Resolver</span>`,
|
||||
onclick: this.handleResolverButtonClick,
|
||||
className: "model-resolver-floating-button"
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -475,6 +475,24 @@ export const optionsMethods = {
|
||||
<span class="mr-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="mr-options-toggle-row">
|
||||
<div class="mr-options-toggle-copy">
|
||||
<span class="mr-options-toggle-title">Show action button above model nodes <span class="mr-tooltip-badge" data-tooltip="When enabled, an action button appears in the selection toolbar above selected model nodes for quick access to Model Resolver actions.">?</span></span>
|
||||
</div>
|
||||
<span class="mr-options-toggle-control">
|
||||
<input id="mr-options-node-toolbox-icon" class="mr-options-switch-input" type="checkbox" ${tokens.node_toolbox_icon_enabled !== false ? 'checked' : ''}>
|
||||
<span class="mr-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="mr-options-toggle-row">
|
||||
<div class="mr-options-toggle-copy">
|
||||
<span class="mr-options-toggle-title">Show Model Resolver in node context menu <span class="mr-tooltip-badge" data-tooltip="When enabled, right-clicking a model node in the graph includes Model Resolver actions in its context menu.">?</span></span>
|
||||
</div>
|
||||
<span class="mr-options-toggle-control">
|
||||
<input id="mr-options-node-context-menu" class="mr-options-switch-input" type="checkbox" ${tokens.node_context_menu_enabled !== false ? 'checked' : ''}>
|
||||
<span class="mr-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="mr-options-db-actions">
|
||||
<button id="mr-options-add-dependency-node" type="button" class="mr-btn mr-btn-secondary">${getSvgIcon('circlePlus')} Add opener node</button>
|
||||
</div>
|
||||
@@ -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),
|
||||
|
||||
@@ -1371,7 +1371,8 @@ export const imageMetadataMethods = {
|
||||
<div class="mr-image-prompt-grid">
|
||||
<div class="mr-image-prompt-side${targetContextMenuAttrs ? ' is-context-menu' : ''}"${targetContextMenuAttrs}>
|
||||
<span class="mr-image-prompt-side-label">
|
||||
Current text field
|
||||
<span class="mr-image-transfer-label-icon mr-image-transfer-label-icon-current" aria-hidden="true">${getSvgIcon('workflow')}</span>
|
||||
<span>Current text field</span>
|
||||
${this.getMetadataTooltipBadge(targetRoleHint, `Current ${row.role} prompt role information`)}
|
||||
</span>
|
||||
${targetHtml}
|
||||
@@ -1379,7 +1380,8 @@ export const imageMetadataMethods = {
|
||||
<span class="mr-image-prompt-arrow" aria-hidden="true">→</span>
|
||||
<div class="mr-image-prompt-side">
|
||||
<span class="mr-image-prompt-side-label">
|
||||
Imported
|
||||
<span class="mr-image-transfer-label-icon mr-image-transfer-label-icon-imported" aria-hidden="true">${getSvgIcon('import')}</span>
|
||||
<span>Imported</span>
|
||||
${this.getMetadataTooltipBadge(sourceRoleHint, `Imported ${row.role} prompt role information`)}
|
||||
</span>
|
||||
${sourceHtml}
|
||||
@@ -2123,12 +2125,18 @@ export const imageMetadataMethods = {
|
||||
return `
|
||||
<div class="mr-image-transfer-change${row.unchanged ? ' is-unchanged' : ''}">
|
||||
<div class="mr-image-transfer-change-side">
|
||||
<span class="mr-image-transfer-change-label">Current</span>
|
||||
<span class="mr-image-transfer-change-label">
|
||||
<span class="mr-image-transfer-label-icon mr-image-transfer-label-icon-current" aria-hidden="true">${getSvgIcon('workflow')}</span>
|
||||
<span>Current</span>
|
||||
</span>
|
||||
<span class="mr-image-transfer-model-value is-current" title="${this.escapeHtml(currentTitle)}"${currentModelAttrs}>${this.escapeHtml(currentLabel)}</span>
|
||||
</div>
|
||||
<span class="mr-image-transfer-change-arrow" aria-hidden="true">→</span>
|
||||
<div class="mr-image-transfer-change-side mr-image-transfer-change-side-imported">
|
||||
<span class="mr-image-transfer-change-label">Imported</span>
|
||||
<span class="mr-image-transfer-change-label">
|
||||
<span class="mr-image-transfer-label-icon mr-image-transfer-label-icon-imported" aria-hidden="true">${getSvgIcon('import')}</span>
|
||||
<span>Imported</span>
|
||||
</span>
|
||||
${importedValueHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2260,12 +2268,18 @@ export const imageMetadataMethods = {
|
||||
return `
|
||||
<div class="mr-image-transfer-change is-unselected">
|
||||
<div class="mr-image-transfer-change-side">
|
||||
<span class="mr-image-transfer-change-label">Current${this.escapeHtml(slotLabel)}</span>
|
||||
<span class="mr-image-transfer-change-label">
|
||||
<span class="mr-image-transfer-label-icon mr-image-transfer-label-icon-current" aria-hidden="true">${getSvgIcon('workflow')}</span>
|
||||
<span>Current${this.escapeHtml(slotLabel)}</span>
|
||||
</span>
|
||||
<span class="mr-image-transfer-model-value is-current" title="${this.escapeHtml(currentValue || currentLabel)}"${currentModelAttrs}>${this.escapeHtml(currentLabel)}</span>
|
||||
</div>
|
||||
<span class="mr-image-transfer-change-arrow" aria-hidden="true">→</span>
|
||||
<div class="mr-image-transfer-change-side mr-image-transfer-change-side-imported">
|
||||
<span class="mr-image-transfer-change-label">Imported</span>
|
||||
<span class="mr-image-transfer-change-label">
|
||||
<span class="mr-image-transfer-label-icon mr-image-transfer-label-icon-imported" aria-hidden="true">${getSvgIcon('import')}</span>
|
||||
<span>Imported</span>
|
||||
</span>
|
||||
<span class="mr-image-transfer-model-value is-placeholder">Select node</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,10 @@ const SVG_ICONS = {
|
||||
windowRestore: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 9h8v8H8z"></path><path d="M6 15H4.5V5.5H14V7"></path></svg>`,
|
||||
x: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"></path><path d="M17.5 6.5l-11 11"></path></svg>`,
|
||||
civitai: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 178 178" aria-hidden="true"><defs><linearGradient id="mr-civitai-gradient" gradientUnits="userSpaceOnUse" x1="89.3" y1="-665.5" x2="89.3" y2="-841.1" gradientTransform="matrix(1 0 0 -1 0 -664)"><stop offset="0" stop-color="#1284F7"></stop><stop offset="1" stop-color="#0A20C9"></stop></linearGradient></defs><path fill="#000" d="M13.3,45.4v87.7l76,43.9l76-43.9V45.4l-76-43.9L13.3,45.4z"></path><path fill="url(#mr-civitai-gradient)" d="M89.3,29.2l52,30v60l-52,30l-52-30v-60L89.3,29.2 M89.3,1.5l-76,43.9v87.8l76,43.9l76-43.9V45.4L89.3,1.5z"></path><path fill="#FFF" d="M104.1,97.2l-14.9,8.5l-14.9-8.5v-17l14.9-8.5l14.9,8.5h18.2V69.7l-33-19l-33,19v38.1l33,19l33-19V97.2H104.1z"></path></svg>`,
|
||||
workflow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="2"></rect><path d="M7 11v4a2 2 0 0 0 2 2h4"></path><rect x="13" y="13" width="8" height="8" rx="2"></rect></svg>`,
|
||||
'import': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m8 11 4 4 4-4"></path><path d="M2 17v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2"></path></svg>`,
|
||||
circleDot: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>`,
|
||||
modelResolver: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17V7l4-2.5 4 3.5 4-3.5L20 7v10l-3.5 2V11L12 15 7.5 11v8L4 17z" fill="{color}" fill-opacity="0.16"></path></svg>`,
|
||||
huggingface: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 88" fill="none" aria-hidden="true"><path fill="#FFD21E" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5Z"></path><path fill="#FF9D0B" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0Zm-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0Z"></path><path fill="#3A3B45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32ZM34.95 32.3c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"></path><path fill="#FF323D" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"></path><path fill="#3A3B45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.61 8.61 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97Z" clip-rule="evenodd"></path><path fill="#FF9D0B" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM24.21 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM17.52 48c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7.1 7.1 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.33 44.33 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48Zm0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85Z"></path><path fill="#FFD21E" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9c-1.87.3-3.24 5.08.68 8.01 3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12Z"></path><path fill="#FF9D0B" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7.1 7.1 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.33 44.33 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48Zm0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.54 1.54 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85Z"></path><path fill="#FFD21E" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44c2.08 2.27-1.58 4.17-6.8 7.33-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12Z"></path></svg>`
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user