322 lines
12 KiB
JavaScript
322 lines
12 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
|
|
app.registerExtension({
|
|
name: "Mudknight Utils.NodeProfiles",
|
|
|
|
async setup() {
|
|
app.ui.settings.addSetting({
|
|
id: "Mudknight Utils.NodeProfiles.ShowButton",
|
|
name: "Show Node Profiles Button",
|
|
type: "boolean",
|
|
defaultValue: false,
|
|
tooltip: "Show/hide the Node Profiles button in the toolbar"
|
|
});
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.profile-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 10000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.profile-modal.show { display: flex; }
|
|
.profile-modal-content {
|
|
background: var(--comfy-menu-bg);
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
}
|
|
#profileList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.profile-node-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
margin-top: 6px;
|
|
}
|
|
.node-tag-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--comfy-menu-bg);
|
|
border: 1px solid var(--border-color);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
}
|
|
.node-tag-type {
|
|
font-weight: bold;
|
|
color: var(--fg-color);
|
|
}
|
|
.node-tag-model {
|
|
color: var(--descrip-text);
|
|
font-size: 10px;
|
|
border-top: 1px solid var(--border-color);
|
|
margin-top: 2px;
|
|
padding-top: 2px;
|
|
}
|
|
.profile-modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.profile-close-btn {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: var(--fg-color);
|
|
}
|
|
.profile-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
background: var(--comfy-input-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
}
|
|
.profile-name {
|
|
flex: 1;
|
|
font-weight: 500;
|
|
color: var(--fg-color);
|
|
cursor: pointer;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
.profile-input-field {
|
|
border-radius: 5px;
|
|
}
|
|
.profile-btn {
|
|
padding: 6px 12px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
background: var(--comfy-menu-bg);
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: var(--fg-color);
|
|
}
|
|
.profile-btn.delete {
|
|
color: var(--error-text);
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'profile-modal';
|
|
modal.id = 'profileModal';
|
|
modal.innerHTML = `
|
|
<div class="profile-modal-content">
|
|
<div class="profile-modal-header">
|
|
<h2>Node Profiles</h2>
|
|
<button class="profile-close-btn">×</button>
|
|
</div>
|
|
<div class="profile-new-section" style="margin-bottom: 20px;">
|
|
<label style="display:block; margin-bottom:8px;">Save Current Parameters</label>
|
|
<div style="display:flex; gap:10px;">
|
|
<input type="text" id="newProfileName" class="profile-input-field" placeholder="Enter profile name..." style="flex:1; padding:8px; background:var(--comfy-input-bg); border:1px solid var(--border-color); color:var(--input-text);">
|
|
<button id="saveProfileBtn" class="profile-save-btn" style="padding:8px 16px; background:var(--primary-bg); color:white; border:none; border-radius:4px; cursor:pointer;">Save</button>
|
|
</div>
|
|
</div>
|
|
<div id="profileList"></div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
const WHITELIST = [
|
|
"BaseNode", "UpscaleNode", "DetailerNode",
|
|
"DetailerPipeNode", "NestedDetailerNode", "NestedDetailerPipeNode"];
|
|
|
|
const formatModelName = (path) => {
|
|
if (!path) return "";
|
|
const filename = path.split('/').pop();
|
|
return filename.split('.')[0];
|
|
};
|
|
|
|
window.saveNodeParams = async (name) => {
|
|
const profileData = {};
|
|
for (const node of app.graph._nodes) {
|
|
if (!node.widgets || !WHITELIST.includes(node.type)) continue;
|
|
|
|
let identifier = node.type;
|
|
let modelName = null;
|
|
|
|
if (node.type.includes("Detailer")) {
|
|
const bbox = node.widgets.find(w => w.name === "bbox_model");
|
|
if (bbox?.value) {
|
|
modelName = bbox.value;
|
|
identifier = `${node.type}:${modelName}`;
|
|
}
|
|
}
|
|
|
|
profileData[identifier] = {
|
|
type: node.type,
|
|
model: modelName,
|
|
widgets: node.widgets.map(w => ({ name: w.name, value: w.value }))
|
|
};
|
|
}
|
|
await fetch(`/node_profiles/${encodeURIComponent(name)}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(profileData)
|
|
});
|
|
};
|
|
|
|
window.restoreNodeParams = async (name) => {
|
|
const response = await fetch(`/node_profiles/${encodeURIComponent(name)}`);
|
|
if (!response.ok) return;
|
|
const profile = await response.json();
|
|
|
|
for (const [id, data] of Object.entries(profile)) {
|
|
let target = null;
|
|
if (data.model) {
|
|
target = app.graph._nodes.find(n =>
|
|
n.type === data.type &&
|
|
n.widgets?.find(w => w.name === "bbox_model")?.value === data.model
|
|
);
|
|
} else {
|
|
target = app.graph._nodes.find(n => n.type === data.type);
|
|
}
|
|
|
|
if (target) {
|
|
data.widgets.forEach(sw => {
|
|
const w = target.widgets.find(nodeW => nodeW.name === sw.name);
|
|
if (w) {
|
|
w.value = sw.value;
|
|
if (w.callback) w.callback(w.value, app.canvas, target);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
app.graph.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
const renderProfiles = async () => {
|
|
const list = document.getElementById('profileList');
|
|
const response = await fetch('/node_profiles');
|
|
const profiles = await response.json();
|
|
list.innerHTML = '';
|
|
|
|
Object.keys(profiles).sort().forEach(name => {
|
|
const item = document.createElement('div');
|
|
item.className = 'profile-item';
|
|
|
|
const tags = Object.entries(profiles[name]).map(([id, data]) => {
|
|
// Strip "Node" from the display name
|
|
const typeLabel = data.type.replace("Node", "");
|
|
const modelLabel = data.model ? formatModelName(data.model) : null;
|
|
|
|
return `
|
|
<div class="node-tag-container">
|
|
<span class="node-tag-type">${typeLabel}</span>
|
|
${modelLabel ? `<span class="node-tag-model">${modelLabel}</span>` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
item.innerHTML = `
|
|
<div style="flex: 1; min-width: 0;">
|
|
<div class="profile-name">${name}</div>
|
|
<div class="profile-node-tags">${tags}</div>
|
|
</div>
|
|
<button class="profile-btn delete">Delete</button>
|
|
`;
|
|
|
|
item.querySelector('.profile-name').onclick = async () => {
|
|
await window.restoreNodeParams(name);
|
|
modal.classList.remove('show');
|
|
};
|
|
|
|
item.querySelector('.delete').onclick = async () => {
|
|
if (confirm(`Delete profile "${name}"?`)) {
|
|
await fetch(`/node_profiles/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
renderProfiles();
|
|
}
|
|
};
|
|
list.appendChild(item);
|
|
});
|
|
};
|
|
|
|
modal.querySelector('.profile-close-btn').onclick = () => modal.classList.remove('show');
|
|
document.getElementById('saveProfileBtn').onclick = async () => {
|
|
const input = document.getElementById('newProfileName');
|
|
if (input.value.trim()) {
|
|
await window.saveNodeParams(input.value.trim());
|
|
input.value = '';
|
|
renderProfiles();
|
|
}
|
|
};
|
|
|
|
const addButton = () => {
|
|
// Check if button should be visible
|
|
const showButton = app.ui.settings.getSettingValue(
|
|
"Mudknight Utils.NodeProfiles.ShowButton"
|
|
);
|
|
|
|
if (!showButton) {
|
|
const existingButton = document.getElementById(
|
|
"node-profiles-button"
|
|
);
|
|
if (existingButton) {
|
|
existingButton.parentElement.remove();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const container = document.querySelector(
|
|
".actionbar-container .flex.gap-2.mx-2"
|
|
);
|
|
if (!container || document.getElementById("node-profiles-button")) return !!container;
|
|
|
|
const profileButton = document.createElement("button");
|
|
profileButton.id = "node-profiles-button";
|
|
profileButton.className = "comfyui-button primary";
|
|
profileButton.style.padding = "0px 10px";
|
|
profileButton.innerHTML = '<i class="mdi mdi-notebook" style="font-size: 24px;"></i>';
|
|
|
|
profileButton.onclick = async () => {
|
|
await renderProfiles();
|
|
modal.classList.add('show');
|
|
};
|
|
|
|
const buttonGroup = document.createElement("div");
|
|
buttonGroup.className = "comfyui-button-group";
|
|
buttonGroup.appendChild(profileButton);
|
|
|
|
const presetBtn = container.querySelector("#preset-manager-button")?.parentElement;
|
|
if (presetBtn) presetBtn.after(buttonGroup);
|
|
else container.prepend(buttonGroup);
|
|
|
|
return true;
|
|
};
|
|
|
|
const attemptAdd = () => { if (!addButton()) setTimeout(attemptAdd, 1000); };
|
|
setTimeout(attemptAdd, 1000);
|
|
|
|
// Watch for setting changes
|
|
const observer = new MutationObserver(() => {
|
|
addButton();
|
|
});
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
});
|