Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,610 @@
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import {deepEqual, addCss, addMeta, isLocalNetwork} from "../common/utils.js";
import {logoIcon, quesitonIcon, rocketIcon, groupIcon, rebootIcon, closeIcon} from "../common/icon.js";
import {$t} from '../common/i18n.js';
import {toast} from "../common/toast.js";
import {$el, ComfyDialog} from "../../../../scripts/ui.js";
addCss('css/index.css')
api.addEventListener("easyuse-toast",event=>{
const content = event.detail.content
const type = event.detail.type
const duration = event.detail.duration
if(!type){
toast.info(content, duration)
}
else{
toast.showToast({
id: `toast-${type}`,
content: `${toast[type+"_icon"]} ${content}`,
duration: duration || 3000,
})
}
})
let draggerEl = null
let isGroupMapcanMove = true
function createGroupMap(){
let div = document.querySelector('#easyuse_groups_map')
if(div){
div.style.display = div.style.display == 'none' ? 'flex' : 'none'
return
}
let groups = app.canvas.graph._groups
let nodes = app.canvas.graph._nodes
let old_nodes = groups.length
div = document.createElement('div')
div.id = 'easyuse_groups_map'
div.innerHTML = ''
let btn = document.createElement('div')
btn.style = `display: flex;
width: calc(100% - 8px);
justify-content: space-between;
align-items: center;
padding: 0 6px;
height: 44px;`
let hideBtn = $el('button.closeBtn',{
innerHTML:closeIcon,
onclick:_=>div.style.display = 'none'
})
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(hideBtn)
textB.style.fontSize = '11px'
textB.innerHTML = `<b>${$t('Groups Map')} (EasyUse)</b>`
div.appendChild(btn)
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
if(isGroupMapcanMove){
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
}
})
function updateGroups(groups, groupsDiv, autoSortDiv){
if(groups.length>0){
autoSortDiv.style.display = 'block'
}else autoSortDiv.style.display = 'none'
for (let index in groups) {
const group = groups[index]
const title = group.title
const show_text = $t('Always')
const hide_text = $t('Bypass')
const mute_text = $t('Never')
let group_item = document.createElement('div')
let group_item_style = `justify-content: space-between;display:flex;background-color: var(--comfy-input-bg);border-radius: 5px;border:1px solid var(--border-color);margin-top:5px;`
group_item.addEventListener("mouseover",event=>{
event.preventDefault()
group_item.style = group_item_style + "filter:brightness(1.2);"
})
group_item.addEventListener("mouseleave",event=>{
event.preventDefault()
group_item.style = group_item_style + "filter:brightness(1);"
})
group_item.addEventListener("dragstart",e=>{
draggerEl = e.currentTarget;
e.currentTarget.style.opacity = "0.6";
e.currentTarget.style.border = "1px dashed yellow";
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setDragImage(emptyImg, 0, 0);
})
group_item.addEventListener("dragend",e=>{
e.target.style.opacity = "1";
e.currentTarget.style.border = "1px dashed transparent";
e.currentTarget.removeAttribute("draggable");
document.querySelectorAll('.easyuse-group-item').forEach((el,i) => {
var prev_i = el.dataset.id;
if (el == draggerEl && prev_i != i ) {
groups.splice(i, 0, groups.splice(prev_i, 1)[0]);
}
el.dataset.id = i;
});
isGroupMapcanMove = true
})
group_item.addEventListener("dragover",e=>{
e.preventDefault();
if (e.currentTarget == draggerEl) return;
let rect = e.currentTarget.getBoundingClientRect();
if (e.clientY > rect.top + rect.height / 2) {
e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget.nextSibling);
} else {
e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget);
}
isGroupMapcanMove = true
})
group_item.setAttribute('data-id',index)
group_item.className = 'easyuse-group-item'
group_item.style = group_item_style
// 标题
let text_group_title = document.createElement('div')
text_group_title.style = `flex:1;font-size:12px;color:var(--input-text);padding:4px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;cursor:pointer`
text_group_title.innerHTML = `${title}`
text_group_title.addEventListener('mousedown',e=>{
isGroupMapcanMove = false
e.currentTarget.parentNode.draggable = 'true';
})
text_group_title.addEventListener('mouseleave',e=>{
setTimeout(_=>{
isGroupMapcanMove = true
},150)
})
group_item.append(text_group_title)
// 按钮组
let buttons = document.createElement('div')
group.recomputeInsideNodes();
const nodesInGroup = group._nodes;
let isGroupShow = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 0
let isGroupMute = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 2
let go_btn = document.createElement('button')
go_btn.style = "margin-right:6px;cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;"
go_btn.innerText = "Go"
go_btn.addEventListener('click', () => {
app.canvas.ds.offset[0] = -group.pos[0] - group.size[0] * 0.5 + (app.canvas.canvas.width * 0.5) / app.canvas.ds.scale;
app.canvas.ds.offset[1] = -group.pos[1] - group.size[1] * 0.5 + (app.canvas.canvas.height * 0.5) / app.canvas.ds.scale;
app.canvas.setDirty(true, true);
app.canvas.setZoom(1)
})
buttons.append(go_btn)
let see_btn = document.createElement('button')
let defaultStyle = `cursor:pointer;font-size:10px;;padding:2px;border: 1px solid var(--border-color);border-radius:4px;width:36px;`
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:var(--theme-color);color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
let pressTimer
let firstTime =0, lastTime =0
let isHolding = false
see_btn.addEventListener('click', () => {
if(isHolding){
isHolding = false
return
}
for (const node of nodesInGroup) {
node.mode = isGroupShow ? 4 : 0;
node.graph.change();
}
isGroupShow = nodesInGroup[0].mode == 0 ? true : false
isGroupMute = nodesInGroup[0].mode == 2 ? true : false
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
})
see_btn.addEventListener('mousedown', () => {
firstTime = new Date().getTime();
clearTimeout(pressTimer);
pressTimer = setTimeout(_=>{
for (const node of nodesInGroup) {
node.mode = isGroupMute ? 0 : 2;
node.graph.change();
}
isGroupShow = nodesInGroup[0].mode == 0 ? true : false
isGroupMute = nodesInGroup[0].mode == 2 ? true : false
see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle)
see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text)
},500)
})
see_btn.addEventListener('mouseup', () => {
lastTime = new Date().getTime();
if(lastTime - firstTime > 500) isHolding = true
clearTimeout(pressTimer);
})
buttons.append(see_btn)
group_item.append(buttons)
groupsDiv.append(group_item)
}
}
let groupsDiv = document.createElement('div')
groupsDiv.id = 'easyuse-groups-items'
groupsDiv.style = `overflow-y: auto;max-height: 400px;height:100%;width: 100%;`
let autoSortDiv = document.createElement('button')
autoSortDiv.style = `cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;`
autoSortDiv.innerText = $t('Auto Sorting')
autoSortDiv.addEventListener('click',e=>{
e.preventDefault()
groupsDiv.innerHTML = ``
let new_groups = groups.sort((a,b)=> a['pos'][0] - b['pos'][0]).sort((a,b)=> a['pos'][1] - b['pos'][1])
updateGroups(new_groups, groupsDiv, autoSortDiv)
})
updateGroups(groups, groupsDiv, autoSortDiv)
div.appendChild(groupsDiv)
let remarkDiv = document.createElement('p')
remarkDiv.style = `text-align:center; font-size:10px; padding:0 10px;color:var(--descrip-text)`
remarkDiv.innerText = $t('Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never')
div.appendChild(groupsDiv)
div.appendChild(remarkDiv)
div.appendChild(autoSortDiv)
let graphDiv = document.getElementById("graph-canvas")
graphDiv.addEventListener('mouseover', async () => {
groupsDiv.innerHTML = ``
let new_groups = app.canvas.graph._groups
updateGroups(new_groups, groupsDiv, autoSortDiv)
old_nodes = nodes
})
if (!document.querySelector('#easyuse_groups_map')){
document.body.appendChild(div)
}else{
div.style.display = 'flex'
}
}
async function cleanup(){
try {
const {Running, Pending} = await api.getQueue()
if(Running.length>0 || Pending.length>0){
toast.error($t("Clean Failed")+ ":"+ $t("Please stop all running tasks before cleaning GPU"))
return
}
api.fetchApi("/easyuse/cleangpu",{
method:"POST"
}).then(res=>{
if(res.status == 200){
toast.success($t("Clean SuccessFully"))
}else{
toast.error($t("Clean Failed"))
}
})
} catch (exception) {}
}
let guideDialog = null
let isDownloading = false
function download_model(url,local_dir){
if(isDownloading || !url || !local_dir) return
isDownloading = true
let body = new FormData();
body.append('url', url);
body.append('local_dir', local_dir);
api.fetchApi("/easyuse/model/download",{
method:"POST",
body
}).then(res=>{
if(res.status == 200){
toast.success($t("Download SuccessFully"))
}else{
toast.error($t("Download Failed"))
}
isDownloading = false
})
}
class GuideDialog {
constructor(note, need_models){
this.dialogDiv = null
this.modelsDiv = null
if(need_models?.length>0){
let tbody = []
for(let i=0;i<need_models.length;i++){
tbody.push($el('tr',[
$el('td',{innerHTML:need_models[i].title || need_models[i].name || ''}),
$el('td',[
need_models[i]['download_url'] ? $el('a',{onclick:_=>download_model(need_models[i]['download_url'],need_models[i]['local_dir']), target:"_blank", textContent:$t('Download Model')}) : '',
need_models[i]['source_url'] ? $el('a',{href:need_models[i]['source_url'], target:"_blank", textContent:$t('Source Url')}) : '',
need_models[i]['desciption'] ? $el('span',{textContent:need_models[i]['desciption']}) : '',
]),
]))
}
this.modelsDiv = $el('div.easyuse-guide-dialog-models.markdown-body',[
$el('h3',{textContent:$t('Models Required')}),
$el('table',{cellpadding:0,cellspacing:0},[
$el('thead',[
$el('tr',[
$el('th',{innerHTML:$t('ModelName')}),
$el('th',{innerHTML:$t('Description')}),
])
]),
$el('tbody',tbody)
])
])
}
this.dialogDiv = $el('div.easyuse-guide-dialog.hidden',[
$el('div.easyuse-guide-dialog-header',[
$el('div.easyuse-guide-dialog-top',[
$el('div.easyuse-guide-dialog-title',{
innerHTML:$t('Workflow Guide')
}),
$el('button.closeBtn',{innerHTML:closeIcon,onclick:_=>this.close()})
]),
$el('div.easyuse-guide-dialog-remark',{
innerHTML:`${$t('Workflow created by')} <a href="https://github.com/yolain/" target="_blank">Yolain</a> , ${$t('Watch more video content')} <a href="https://space.bilibili.com/1840885116" target="_blank">B站乱乱呀</a>`
})
]),
$el('div.easyuse-guide-dialog-content.markdown-body',[
$el('div.easyuse-guide-dialog-note',{
innerHTML:note
}),
...this.modelsDiv ? [this.modelsDiv] : []
])
])
if(disableRenderInfo){
this.dialogDiv.classList.add('disable-render-info')
}
document.body.appendChild(this.dialogDiv)
}
show(){
if(this.dialogDiv) this.dialogDiv.classList.remove('hidden')
}
close(){
if(this.dialogDiv){
this.dialogDiv.classList.add('hidden')
}
}
toggle(){
if(this.dialogDiv){
if(this.dialogDiv.classList.contains('hidden')){
this.show()
}else{
this.close()
}
}
}
remove(){
if(this.dialogDiv) document.body.removeChild(this.dialogDiv)
}
}
// toolbar
const toolBarId = "Comfy.EasyUse.toolBar"
const getEnableToolBar = _ => app.ui.settings.getSettingValue(toolBarId, true)
const getNewMenuPosition = _ => {
try{
return app.ui.settings.getSettingValue('Comfy.UseNewMenu', 'Disabled')
}catch (e){
return 'Disabled'
}
}
let note = null
let toolbar = null
let enableToolBar = getEnableToolBar() && getNewMenuPosition() == 'Disabled'
let disableRenderInfo = localStorage['Comfy.Settings.Comfy.EasyUse.disableRenderInfo'] ? true : false
export function addToolBar(app) {
app.ui.settings.addSetting({
id: toolBarId,
name: $t("Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)"),
type: "boolean",
defaultValue: enableToolBar,
onChange(value) {
enableToolBar = !!value;
if(enableToolBar){
showToolBar()
}else hideToolBar()
},
});
}
function showToolBar(){
if(toolbar) toolbar.style.display = 'flex'
}
function hideToolBar(){
if(toolbar) toolbar.style.display = 'none'
}
let monitor = null
function setCrystoolsUI(position){
const crystools = document.getElementById('crystools-root')?.children || null
if(crystools?.length>0){
if(!monitor){
for (let i = 0; i < crystools.length; i++) {
if (crystools[i].id === 'crystools-monitor-container') {
monitor = crystools[i];
break;
}
}
}
if(monitor){
if(position == 'Disabled'){
let replace = true
for (let i = 0; i < crystools.length; i++) {
if (crystools[i].id === 'crystools-monitor-container') {
replace = false
break;
}
}
document.getElementById('crystools-root').appendChild(monitor)
}
else {
let monitor_div = document.getElementById('comfyui-menu-monitor')
if(!monitor_div) app.menu.settingsGroup.element.before($el('div',{id:'comfyui-menu-monitor'},monitor))
else monitor_div.appendChild(monitor)
}
}
}
}
const changeNewMenuPosition = app.ui.settings.settingsLookup?.['Comfy.UseNewMenu']
if(changeNewMenuPosition) changeNewMenuPosition.onChange = v => {
v == 'Disabled' ? showToolBar() : hideToolBar()
setCrystoolsUI(v)
}
app.registerExtension({
name: "comfy.easyUse",
init() {
// Canvas Menu
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = getCanvasMenuOptions.apply(this, arguments);
let emptyImg = new Image()
emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
options.push(null,
// Groups Map
{
content: groupIcon.replace('currentColor','var(--warning-color)') + ' '+ $t('Groups Map') + ' (EasyUse)',
callback: async() => {
createGroupMap()
}
},
// Force clean ComfyUI GPU Used 强制卸载模型GPU占用
{
content: rocketIcon.replace('currentColor','var(--theme-color-light)') + ' '+ $t('Cleanup Of GPU Usage') + ' (EasyUse)',
callback: async() =>{
await cleanup()
}
},
// Only show the reboot option if the server is running on a local network 仅在本地或局域网环境可重启服务
isLocalNetwork(window.location.host) ? {
content: rebootIcon.replace('currentColor','var(--error-color)') + ' '+ $t('Reboot ComfyUI') + ' (EasyUse)',
callback: _ =>{
if (confirm($t("Are you sure you'd like to reboot the server?"))){
try {
api.fetchApi("/easyuse/reboot");
} catch (exception) {}
}
}
} : null,
);
return options;
};
let renderInfoEvent = LGraphCanvas.prototype.renderInfo
if(disableRenderInfo){
LGraphCanvas.prototype.renderInfo = function (ctx, x, y) {}
}
if(!toolbar){
toolbar = $el('div.easyuse-toolbar',[
$el('div.easyuse-toolbar-item',{
onclick:_=>{
createGroupMap()
}
},[
$el('div.easyuse-toolbar-icon.group', {innerHTML:groupIcon}),
$el('div.easyuse-toolbar-tips',$t('Groups Map'))
]),
$el('div.easyuse-toolbar-item',{
onclick:async()=>{
await cleanup()
}
},[
$el('div.easyuse-toolbar-icon.rocket',{innerHTML:rocketIcon}),
$el('div.easyuse-toolbar-tips',$t('Cleanup Of GPU Usage'))
]),
])
if(disableRenderInfo){
toolbar.classList.add('disable-render-info')
}else{
toolbar.classList.remove('disable-render-info')
}
document.body.appendChild(toolbar)
}
// rewrite handleFile
let loadGraphDataEvent = app.loadGraphData
app.loadGraphData = async function (data, clean=true) {
// if(data?.extra?.cpr){
// toast.copyright()
// }
if(data?.extra?.note){
if(guideDialog) {
guideDialog.remove()
guideDialog = null
}
if(note && toolbar) toolbar.removeChild(note)
const need_models = data.extra?.need_models || null
guideDialog = new GuideDialog(data.extra.note, need_models)
note = $el('div.easyuse-toolbar-item',{
onclick:async()=>{
guideDialog.toggle()
}
},[
$el('div.easyuse-toolbar-icon.question',{innerHTML:quesitonIcon}),
$el('div.easyuse-toolbar-tips',$t('Workflow Guide'))
])
if(toolbar) toolbar.insertBefore(note, toolbar.firstChild)
}
else{
if(note) {
toolbar.removeChild(note)
note = null
}
}
return await loadGraphDataEvent.apply(this, [...arguments])
}
addToolBar(app)
},
async setup() {
// New style menu button
if(app.menu?.actionsGroup){
const groupMap = new (await import('../../../../scripts/ui/components/button.js')).ComfyButton({
icon:'list-box',
action:()=> createGroupMap(),
tooltip: "EasyUse Group Map",
// content: "EasyUse Group Map",
classList: "comfyui-button comfyui-menu-mobile-collapse"
});
app.menu.actionsGroup.element.after(groupMap.element);
const position = getNewMenuPosition()
setCrystoolsUI(position)
if(position == 'Disabled') showToolBar()
else hideToolBar()
// const easyNewMenu = $el('div.easyuse-new-menu',[
// $el('div.easyuse-new-menu-intro',[
// $el('div.easyuse-new-menu-logo',{innerHTML:logoIcon}),
// $el('div.easyuse-new-menu-title',[
// $el('div.title',{textContent:'ComfyUI-Easy-Use'}),
// $el('div.desc',{textContent:'Version:'})
// ])
// ])
// ])
// app.menu?.actionsGroup.element.after(new (await import('../../../../scripts/ui/components/splitButton.js')).ComfySplitButton({
// primary: groupMap,
// mode:'click',
// position:'absolute',
// horizontal: 'right'
// },easyNewMenu).element);
}
},
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name.startsWith("easy")) {
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
return r;
};
}
},
});

View File

@@ -0,0 +1,283 @@
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { $t } from '../common/i18n.js'
import { toast } from "../common/toast.js";
import {sleep, accSub} from "../common/utils.js";
let api_keys = []
let api_current = 0
let user_info = {}
const api_cost = {
'sd3': 6.5,
'sd3-turbo': 4,
}
class AccountDialog extends ComfyDialog {
constructor() {
super();
this.lists = []
this.dialog_div = null
this.user_div = null
}
addItem(index, user_div){
return $el('div.easyuse-account-dialog-item',[
$el('input',{type:'text',placeholder:'Enter name',oninput: e=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
api_keys[dataIndex]['name'] = e.target.value
},value:api_keys[index]['name']}),
$el('input.key',{type:'text',oninput: e=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
api_keys[dataIndex]['key'] = e.target.value
},placeholder:'Enter APIKEY', value:api_keys[index]['key']}),
$el('button.choose',{textContent:$t('Choose'),onclick:async(e)=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
let name = api_keys[dataIndex]['name']
let key = api_keys[dataIndex]['key']
if(!name){
toast.error($t('Please enter the account name'))
return
}
else if(!key){
toast.error($t('Please enter the APIKEY'))
return
}
let missing = true
for(let i=0;i<api_keys.length;i++){
if(!api_keys[i].key) {
missing = false
break
}
}
if(!missing){
toast.error($t('APIKEY is not Empty'))
return
}
// 保存记录
api_current = dataIndex
const body = new FormData();
body.append('api_keys', JSON.stringify(api_keys));
body.append('current',api_current)
const res = await api.fetchApi('/easyuse/stability/set_api_keys', {
method: 'POST',
body
})
if (res.status == 200) {
const data = await res.json()
if(data?.account && data?.balance){
const avatar = data.account?.profile_picture || null
const email = data.account?.email || null
const credits = data.balance?.credits || 0
user_div.replaceChildren(
$el('div.easyuse-account-user-info', {
onclick:_=>{
new AccountDialog().show(user_div);
}
},[
$el('div.user',[
$el('div.avatar', avatar ? [$el('img',{src:avatar})] : '😀'),
$el('div.info', [
$el('h5.name', email),
$el('h6.remark','Credits: '+ credits)
])
]),
$el('div.edit', {textContent:$t('Edit')})
])
)
toast.success($t('Save Succeed'))
}
else toast.success($t('Save Succeed'))
this.close()
} else {
toast.error($t('Save Failed'))
}
}}),
$el('button.delete',{textContent:$t('Delete'),onclick:e=>{
const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode)
if(api_keys.length<=1){
toast.error($t('At least one account is required'))
return
}
api_keys.splice(dataIndex,1)
this.dialog_div.removeChild(e.target.parentNode)
}}),
])
}
show(userdiv) {
api_keys.forEach((item,index)=>{
this.lists.push(this.addItem(index,userdiv))
})
this.dialog_div = $el("div.easyuse-account-dialog", this.lists)
super.show(
$el('div.easyuse-account-dialog-main',[
$el('div',[
$el('a',{href:'https://platform.stability.ai/account/keys',target:'_blank',textContent:$t('Getting Your APIKEY')}),
]),
this.dialog_div,
])
);
}
createButtons() {
const btns = super.createButtons();
btns.unshift($el('button',{
type:'button',
textContent:$t('Save Account Info'),
onclick:_=>{
let missing = true
for(let i=0;i<api_keys.length;i++){
if(!api_keys[i].key) {
missing = false
break
}
}
if(!missing){
toast.error($t('APIKEY is not Empty'))
}
else {
const body = new FormData();
body.append('api_keys', JSON.stringify(api_keys));
api.fetchApi('/easyuse/stability/set_api_keys', {
method: 'POST',
body
}).then(res => {
if (res.status == 200) {
toast.success($t('Save Succeed'))
} else {
toast.error($t('Save Failed'))
}
})
}
}
}))
btns.unshift($el('button',{
type:'button',
textContent:$t('Add Account'),
onclick:_=>{
const name = 'Account '+(api_keys.length).toString()
api_keys.push({name,key:''})
const item = this.addItem(api_keys.length - 1)
this.lists.push(item)
this.dialog_div.appendChild(item)
}
}))
return btns
}
}
app.registerExtension({
name: 'comfy.easyUse.account',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData.name == 'easy stableDiffusion3API'){
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function() {
onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined;
const seed_widget = this.widgets.find(w => ['seed_num','seed'].includes(w.name))
const seed_control = this.widgets.find(w=> ['control_before_generate','control_after_generate'].includes(w.name))
let model_widget = this.widgets.find(w => w.name == 'model')
model_widget.callback = value =>{
cost_widget.value = '-'+api_cost[value]
}
const cost_widget = this.addWidget('text', 'cost_credit', '0', _=>{
},{
serialize:false,
})
cost_widget.disabled = true
setTimeout(_=>{
if(seed_control.name == 'control_before_generate' && seed_widget.value === 0){
seed_widget.value = Math.floor(Math.random() * 4294967294)
}
cost_widget.value = '-'+api_cost[model_widget.value]
},100)
let user_div = $el('div.easyuse-account-user', [$t('Loading UserInfo...')])
let account = this.addDOMWidget('account',"btn",$el('div.easyuse-account',user_div));
// 更新balance信息
api.addEventListener('stable-diffusion-api-generate-succeed', async ({detail}) => {
let remarkDiv = user_div.querySelectorAll('.remark')
if(remarkDiv && remarkDiv[0]){
const credits = detail?.model ? api_cost[detail.model] : 0
if(credits) {
let balance = accSub(parseFloat(remarkDiv[0].innerText.replace(/Credits: /g,'')),credits)
if(balance>0){
remarkDiv[0].innerText = 'Credits: '+ balance.toString()
}
}
}
await sleep(10000)
const res = await api.fetchApi('/easyuse/stability/balance')
if(res.status == 200){
const data = await res.json()
if(data?.balance){
const credits = data.balance?.credits || 0
if(remarkDiv && remarkDiv[0]){
remarkDiv[0].innerText = 'Credits: ' + credits
}
}
}
})
// 获取api_keys
const res = await api.fetchApi('/easyuse/stability/api_keys')
if (res.status == 200){
let data = await res.json()
api_keys = data.keys
api_current = data.current
if (api_keys.length > 0 && api_current!==undefined){
const api_key = api_keys[api_current]['key']
const api_name = api_keys[api_current]['name']
if(!api_key){
user_div.replaceChildren(
$el('div.easyuse-account-user-info', {
onclick:_=>{
new AccountDialog().show(user_div);
}
},[
$el('div.user',[
$el('div.avatar', '😀'),
$el('div.info', [
$el('h5.name', api_name),
$el('h6.remark',$t('Click to set the APIKEY first'))
])
]),
$el('div.edit', {textContent:$t('Edit')})
])
)
}else{
// 获取账号信息
const res = await api.fetchApi('/easyuse/stability/user_info')
if(res.status == 200){
const data = await res.json()
if(data?.account && data?.balance){
const avatar = data.account?.profile_picture || null
const email = data.account?.email || null
const credits = data.balance?.credits || 0
user_div.replaceChildren(
$el('div.easyuse-account-user-info', {
onclick:_=>{
new AccountDialog().show(user_div);
}
},[
$el('div.user',[
$el('div.avatar', avatar ? [$el('img',{src:avatar})] : '😀'),
$el('div.info', [
$el('h5.name', email),
$el('h6.remark','Credits: '+ credits)
])
]),
$el('div.edit', {textContent:$t('Edit')})
])
)
}
}
}
}
}
}
}
}
})

View File

@@ -0,0 +1,174 @@
import {app} from "../../../../scripts/app.js";
import {api} from "../../../../scripts/api.js";
import {$el} from "../../../../scripts/ui.js";
import {$t} from "../common/i18n.js";
import {getExtension, spliceExtension} from '../common/utils.js'
import {toast} from "../common/toast.js";
const setting_id = "Comfy.EasyUse.MenuNestSub"
let enableMenuNestSub = false
let thumbnails = []
export function addMenuNestSubSetting(app) {
app.ui.settings.addSetting({
id: setting_id,
name: $t("Enable ContextMenu Auto Nest Subdirectories (ComfyUI-Easy-Use)"),
type: "boolean",
defaultValue: enableMenuNestSub,
onChange(value) {
enableMenuNestSub = !!value;
},
});
}
const getEnableMenuNestSub = _ => app.ui.settings.getSettingValue(setting_id, enableMenuNestSub)
const Loaders = ['easy fullLoader','easy a1111Loader','easy comfyLoader']
app.registerExtension({
name:"comfy.easyUse.contextMenu",
async setup(app){
addMenuNestSubSetting(app)
// 获取所有模型图像
const imgRes = await api.fetchApi(`/easyuse/models/thumbnail`)
if (imgRes.status === 200) {
let data = await imgRes.json();
thumbnails = data
}
else if(getEnableMenuNestSub()){
toast.error($t("Too many thumbnails, have closed the display"))
}
const existingContextMenu = LiteGraph.ContextMenu;
LiteGraph.ContextMenu = function(values,options){
const threshold = 10;
const enabled = getEnableMenuNestSub();
if(!enabled || (values?.length || 0) <= threshold || !(options?.callback) || values.some(i => typeof i !== 'string')){
if(enabled){
// console.log('Skipping context menu auto nesting for incompatible menu.');
}
return existingContextMenu.apply(this,[...arguments]);
}
const compatValues = values;
const originalValues = [...compatValues];
const folders = {};
const specialOps = [];
const folderless = [];
for(const value of compatValues){
const splitBy = value.indexOf('/') > -1 ? '/' : '\\';
const valueSplit = value.split(splitBy);
if(valueSplit.length > 1){
const key = valueSplit.shift();
folders[key] = folders[key] || [];
folders[key].push(valueSplit.join(splitBy));
}else if(value === 'CHOOSE' || value.startsWith('DISABLE ')){
specialOps.push(value);
}else{
folderless.push(value);
}
}
const foldersCount = Object.values(folders).length;
if(foldersCount > 0){
const oldcallback = options.callback;
options.callback = null;
const newCallback = (item,options) => {
if(['None','无','無','なし'].includes(item.content)) oldcallback('None',options)
else oldcallback(originalValues.find(i => i.endsWith(item.content),options));
};
const addContent = (content, folderName='') => {
const name = folderName ? folderName + '\\' + spliceExtension(content) : spliceExtension(content);
const ext = getExtension(content)
const time = new Date().getTime()
let thumbnail = ''
if(['ckpt', 'pt', 'bin', 'pth', 'safetensors'].includes(ext)){
for(let i=0;i<thumbnails.length;i++){
let thumb = thumbnails[i]
if(name && thumb && thumb.indexOf(name) != -1){
thumbnail = thumbnails[i]
break
}
}
}
let newContent
if(thumbnail){
const protocol = window.location.protocol
const host = window.location.host
const base_url = `${protocol}//${host}`
const thumb_url = thumbnail.replace(':','%3A').replace(/\\/g,'/')
newContent = $el("div.easyuse-model", {},[$el("span",{},content + ' *'),$el("img",{src:`${base_url}/${thumb_url}?t=${time}`})])
}else{
newContent = $el("div.easyuse-model", {},[
$el("span",{},content)
])
}
return {
content,
title:newContent.outerHTML,
callback: newCallback
}
}
const newValues = [];
const add_sub_folder = (folder, folderName) => {
let subs = []
let less = []
const b = folder.map(name=> {
const _folders = {};
const splitBy = name.indexOf('/') > -1 ? '/' : '\\';
const valueSplit = name.split(splitBy);
if(valueSplit.length > 1){
const key = valueSplit.shift();
_folders[key] = _folders[key] || [];
_folders[key].push(valueSplit.join(splitBy));
}
const foldersCount = Object.values(folders).length;
if(foldersCount > 0){
let key = Object.keys(_folders)[0]
if(key && _folders[key]) subs.push({key, value:_folders[key][0]})
else{
less.push(addContent(name,key))
}
}
return addContent(name,folderName)
})
if(subs.length>0){
let subs_obj = {}
subs.forEach(item => {
subs_obj[item.key] = subs_obj[item.key] || []
subs_obj[item.key].push(item.value)
})
return [...Object.entries(subs_obj).map(f => {
return {
content: f[0],
has_submenu: true,
callback: () => {},
submenu: {
options: add_sub_folder(f[1], f[0]),
}
}
}),...less]
}
else return b
}
for(const [folderName,folder] of Object.entries(folders)){
newValues.push({
content:folderName,
has_submenu:true,
callback:() => {},
submenu:{
options:add_sub_folder(folder,folderName),
}
});
}
newValues.push(...folderless.map(f => addContent(f, '')));
if(specialOps.length > 0) newValues.push(...specialOps.map(f => addContent(f, '')));
return existingContextMenu.call(this,newValues,options);
}
return existingContextMenu.apply(this,[...arguments]);
}
LiteGraph.ContextMenu.prototype = existingContextMenu.prototype;
},
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,593 @@
import {app} from "../../../../scripts/app.js";
import {$t} from '../common/i18n.js'
import {CheckpointInfoDialog, LoraInfoDialog} from "../common/model.js";
const loaders = ['easy fullLoader', 'easy a1111Loader', 'easy comfyLoader', 'easy kolorsLoader', 'easy hunyuanDiTLoader', 'easy pixArtLoader']
const preSampling = ['easy preSampling', 'easy preSamplingAdvanced', 'easy preSamplingDynamicCFG', 'easy preSamplingNoiseIn', 'easy preSamplingCustom', 'easy preSamplingLayerDiffusion', 'easy fullkSampler']
const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy kSamplerInpainting', 'easy kSamplerDownscaleUnet', 'easy kSamplerLayerDiffusion']
const controlnet = ['easy controlnetLoader', 'easy controlnetLoaderADV', 'easy controlnetLoader++', 'easy instantIDApply', 'easy instantIDApplyADV']
const ipadapter = ['easy ipadapterApply', 'easy ipadapterApplyADV', 'easy ipadapterApplyFaceIDKolors', 'easy ipadapterStyleComposition', 'easy ipadapterApplyFromParams', 'easy pulIDApply', 'easy pulIDApplyADV']
const positive_prompt = ['easy positive', 'easy wildcards']
const imageNode = ['easy loadImageBase64', 'LoadImage', 'LoadImageMask']
const inpaint = ['easy applyBrushNet', 'easy applyPowerPaint', 'easy applyInpaint']
const widgetMapping = {
"positive_prompt":{
"text": "positive",
"positive": "text"
},
"loaders":{
"ckpt_name": "ckpt_name",
"vae_name": "vae_name",
"clip_skip": "clip_skip",
"lora_name": "lora_name",
"resolution": "resolution",
"empty_latent_width": "empty_latent_width",
"empty_latent_height": "empty_latent_height",
"positive": "positive",
"negative": "negative",
"batch_size": "batch_size",
"a1111_prompt_style": "a1111_prompt_style"
},
"preSampling":{
"steps": "steps",
"cfg": "cfg",
"cfg_scale_min": "cfg",
"sampler_name": "sampler_name",
"scheduler": "scheduler",
"denoise": "denoise",
"seed_num": "seed_num",
"seed": "seed"
},
"kSampler":{
"image_output": "image_output",
"save_prefix": "save_prefix",
"link_id": "link_id"
},
"controlnet":{
"control_net_name":"control_net_name",
"strength": ["strength", "cn_strength"],
"scale_soft_weights": ["scale_soft_weights","cn_soft_weights"],
"cn_strength": ["strength", "cn_strength"],
"cn_soft_weights": ["scale_soft_weights","cn_soft_weights"],
},
"ipadapter":{
"preset":"preset",
"lora_strength": "lora_strength",
"provider": "provider",
"weight":"weight",
"weight_faceidv2": "weight_faceidv2",
"start_at": "start_at",
"end_at": "end_at",
"cache_mode": "cache_mode",
"use_tiled": "use_tiled",
"insightface": "insightface",
"pulid_file": "pulid_file"
},
"load_image":{
"image":"image",
"base64_data":"base64_data",
"channel": "channel"
},
"inpaint":{
"dtype": "dtype",
"fitting": "fitting",
"function": "function",
"scale": "scale",
"start_at": "start_at",
"end_at": "end_at"
}
}
const inputMapping = {
"loaders":{
"optional_lora_stack": "optional_lora_stack",
"positive": "positive",
"negative": "negative"
},
"preSampling":{
"pipe": "pipe",
"image_to_latent": "image_to_latent",
"latent": "latent"
},
"kSampler":{
"pipe": "pipe",
"model": "model"
},
"controlnet":{
"pipe": "pipe",
"image": "image",
"image_kps": "image_kps",
"control_net": "control_net",
"positive": "positive",
"negative": "negative",
"mask": "mask"
},
"positive_prompt":{
},
"ipadapter":{
"model":"model",
"image":"image",
"image_style": "image",
"attn_mask":"attn_mask",
"optional_ipadapter":"optional_ipadapter"
},
"inpaint":{
"pipe": "pipe",
"image": "image",
"mask": "mask"
}
};
const outputMapping = {
"loaders":{
"pipe": "pipe",
"model": "model",
"vae": "vae",
"clip": null,
"positive": null,
"negative": null,
"latent": null,
},
"preSampling":{
"pipe":"pipe"
},
"kSampler":{
"pipe": "pipe",
"image": "image"
},
"controlnet":{
"pipe": "pipe",
"positive": "positive",
"negative": "negative"
},
"positive_prompt":{
"text": "positive",
"positive": "text"
},
"load_image":{
"IMAGE":"IMAGE",
"MASK": "MASK"
},
"ipadapter":{
"model":"model",
"tiles":"tiles",
"masks":"masks",
"ipadapter":"ipadapter"
},
"inpaint":{
"pipe": "pipe",
}
};
// 替换节点
function replaceNode(oldNode, newNodeName, type) {
const newNode = LiteGraph.createNode(newNodeName);
if (!newNode) {
return;
}
app.graph.add(newNode);
newNode.pos = oldNode.pos.slice();
newNode.size = oldNode.size.slice();
oldNode.widgets.forEach(widget => {
if(widgetMapping[type][widget.name]){
const newName = widgetMapping[type][widget.name];
if (newName) {
const newWidget = findWidgetByName(newNode, newName);
if (newWidget) {
newWidget.value = widget.value;
if(widget.name == 'seed_num'){
newWidget.linkedWidgets[0].value = widget.linkedWidgets[0].value
}
if(widget.type == 'converted-widget'){
convertToInput(newNode, newWidget, widget);
}
}
}
}
});
if(oldNode.inputs){
oldNode.inputs.forEach((input, index) => {
if (input && input.link && inputMapping[type][input.name]) {
const newInputName = inputMapping[type][input.name];
// If the new node does not have this output, skip
if (newInputName === null) {
return;
}
const newInputIndex = newNode.findInputSlot(newInputName);
if (newInputIndex !== -1) {
const originLinkInfo = oldNode.graph.links[input.link];
if (originLinkInfo) {
const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
if (originNode) {
originNode.connect(originLinkInfo.origin_slot, newNode, newInputIndex);
}
}
}
}
});
}
if(oldNode.outputs){
oldNode.outputs.forEach((output, index) => {
if (output && output.links && outputMapping[type] && outputMapping[type][output.name]) {
const newOutputName = outputMapping[type][output.name];
// If the new node does not have this output, skip
if (newOutputName === null) {
return;
}
const newOutputIndex = newNode.findOutputSlot(newOutputName);
if (newOutputIndex !== -1) {
output.links.forEach(link => {
const targetLinkInfo = oldNode.graph.links[link];
if (targetLinkInfo) {
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
if (targetNode) {
newNode.connect(newOutputIndex, targetNode, targetLinkInfo.target_slot);
}
}
});
}
}
});
}
// Remove old node
app.graph.remove(oldNode);
// Remove others
if(newNode.type == 'easy fullkSampler'){
const link_output_id = newNode.outputs[0].links
if(link_output_id && link_output_id[0]){
const nodes = app.graph._nodes
const node = nodes.find(cate=> cate.inputs && cate.inputs[0] && cate.inputs[0]['link'] == link_output_id[0])
if(node){
app.graph.remove(node);
}
}
}else if(preSampling.includes(newNode.type)){
const link_output_id = newNode.outputs[0].links
if(!link_output_id || !link_output_id[0]){
const ksampler = LiteGraph.createNode('easy kSampler');
app.graph.add(ksampler);
ksampler.pos = newNode.pos.slice();
ksampler.pos[0] = ksampler.pos[0] + newNode.size[0] + 20;
const newInputIndex = newNode.findInputSlot('pipe');
if (newInputIndex !== -1) {
if (newNode) {
newNode.connect(0, ksampler, newInputIndex);
}
}
}
}
// autoHeight
newNode.setSize([newNode.size[0], newNode.computeSize()[1]]);
}
export function findWidgetByName(node, widgetName) {
return node.widgets.find(widget => typeof widgetName == 'object' ? widgetName.includes(widget.name) : widget.name === widgetName);
}
function replaceNodeMenuCallback(currentNode, targetNodeName, type) {
return function() {
replaceNode(currentNode, targetNodeName, type);
};
}
const addMenuHandler = (nodeType, cb)=> {
const getOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments);
cb.apply(this, arguments);
return r;
};
}
const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => {
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: content,
has_submenu: has_submenu,
callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include)
})
if(type == 'loaders') {
options.unshift({
content: $t("💎 View Lora Info..."),
callback: (value, options, e, menu, node) => {
const widget = node.widgets.find(cate => cate.name == 'lora_name')
let name = widget.value;
if (!name || name == 'None') return
new LoraInfoDialog(name).show('loras', name);
}
})
options.unshift({
content: $t("💎 View Checkpoint Info..."),
callback: (value, options, e, menu, node) => {
let name = node.widgets[0].value;
if (!name || name == 'None') return
new CheckpointInfoDialog(name).show('checkpoints', name);
}
})
}
})
}
const showSwapMenu = (value, options, e, menu, node, type, nodes_include) => {
const swapOptions = [];
nodes_include.map(cate=>{
if (node.type !== cate) {
swapOptions.push({
content: `${cate}`,
callback: replaceNodeMenuCallback(node, cate, type)
});
}
})
new LiteGraph.ContextMenu(swapOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false;
}
// 重载节点
const CONVERTED_TYPE = "converted-widget";
const GET_CONFIG = Symbol();
function hideWidget(node, widget, suffix = "") {
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix;
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
if (!node.inputs) {
return undefined;
}
let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
if (!node_input || !node_input.link) {
return undefined;
}
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
};
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name);
}
}
}
function convertToInput(node, widget, config) {
console.log('config:', config)
hideWidget(node, widget);
const { type } = getWidgetType(config);
// Add input and store widget config for creating on primitive node
const sz = node.size;
if(!widget.options || !widget.options.forceInput){
node.addInput(widget.name, type, {
widget: { name: widget.name, [GET_CONFIG]: () => config },
});
}
for (const widget of node.widgets) {
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
}
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
}
function getWidgetType(config) {
// Special handling for COMBO so we restrict links based on the entries
let type = config[0];
if (type instanceof Array) {
type = "COMBO";
}
return { type };
}
const reloadNode = function (node) {
const nodeType = node.constructor.type;
const origVals = node.properties.origVals || {};
const nodeTitle = origVals.title || node.title;
const nodeColor = origVals.color || node.color;
const bgColor = origVals.bgcolor || node.bgcolor;
const oldNode = node
const options = {
'size': [...node.size],
'color': nodeColor,
'bgcolor': bgColor,
'pos': [...node.pos]
}
let inputLinks = []
let outputLinks = []
if(node.inputs){
for (const input of node.inputs) {
if (input.link) {
const input_name = input.name
const input_slot = node.findInputSlot(input_name)
const input_node = node.getInputNode(input_slot)
const input_link = node.getInputLink(input_slot)
inputLinks.push([input_link.origin_slot, input_node, input_name])
}
}
}
if(node.outputs) {
for (const output of node.outputs) {
if (output.links) {
const output_name = output.name
for (const linkID of output.links) {
const output_link = graph.links[linkID]
const output_node = graph._nodes_by_id[output_link.target_id]
outputLinks.push([output_name, output_node, output_link.target_slot])
}
}
}
}
app.graph.remove(node)
const newNode = app.graph.add(LiteGraph.createNode(nodeType, nodeTitle, options));
function handleLinks() {
// re-convert inputs
if(oldNode.widgets) {
for (let w of oldNode.widgets) {
if (w.type === 'converted-widget') {
const WidgetToConvert = newNode.widgets.find((nw) => nw.name === w.name);
for (let i of oldNode.inputs) {
if (i.name === w.name) {
convertToInput(newNode, WidgetToConvert, i.widget);
}
}
}
}
}
// replace input and output links
for (let input of inputLinks) {
const [output_slot, output_node, input_name] = input;
output_node.connect(output_slot, newNode.id, input_name)
}
for (let output of outputLinks) {
const [output_name, input_node, input_slot] = output;
newNode.connect(output_name, input_node, input_slot)
}
}
// fix widget values
let values = oldNode.widgets_values;
if (!values && newNode.widgets?.length>0) {
newNode.widgets.forEach((newWidget, index) => {
const oldWidget = oldNode.widgets[index];
if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) {
newWidget.value = oldWidget.value;
}
});
handleLinks();
return;
}
let pass = false
const isIterateForwards = values?.length <= newNode.widgets?.length;
let vi = isIterateForwards ? 0 : values.length - 1;
function evalWidgetValues(testValue, newWidg) {
if (testValue === true || testValue === false) {
if (newWidg.options?.on && newWidg.options?.off) {
return { value: testValue, pass: true };
}
} else if (typeof testValue === "number") {
if (newWidg.options?.min <= testValue && testValue <= newWidg.options?.max) {
return { value: testValue, pass: true };
}
} else if (newWidg.options?.values?.includes(testValue)) {
return { value: testValue, pass: true };
} else if (newWidg.inputEl && typeof testValue === "string") {
return { value: testValue, pass: true };
}
return { value: newWidg.value, pass: false };
}
const updateValue = (wi) => {
const oldWidget = oldNode.widgets[wi];
let newWidget = newNode.widgets[wi];
if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) {
while ((isIterateForwards ? vi < values.length : vi >= 0) && !pass) {
let { value, pass } = evalWidgetValues(values[vi], newWidget);
if (pass && value !== null) {
newWidget.value = value;
break;
}
vi += isIterateForwards ? 1 : -1;
}
vi++
if (!isIterateForwards) {
vi = values.length - (newNode.widgets?.length - 1 - wi);
}
}
};
if (isIterateForwards && newNode.widgets?.length>0) {
for (let wi = 0; wi < newNode.widgets.length; wi++) {
updateValue(wi);
}
} else if(newNode.widgets?.length>0){
for (let wi = newNode.widgets.length - 1; wi >= 0; wi--) {
updateValue(wi);
}
}
handleLinks();
};
app.registerExtension({
name: "comfy.easyUse.extraMenu",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// 刷新节点
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: $t("🔃 Reload Node"),
callback: (value, options, e, menu, node) => {
let graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
reloadNode(node);
} else {
for (let i in graphcanvas.selected_nodes) {
reloadNode(graphcanvas.selected_nodes[i]);
}
}
}
})
// ckptNames
if(nodeData.name == 'easy ckptNames'){
options.unshift({
content: $t("💎 View Checkpoint Info..."),
callback: (value, options, e, menu, node) => {
let name = node.widgets[0].value;
if (!name || name == 'None') return
new CheckpointInfoDialog(name).show('checkpoints', name);
}
})
}
})
// Swap提示词
if (positive_prompt.includes(nodeData.name)) {
addMenu("↪️ Swap EasyPrompt", 'positive_prompt', positive_prompt, nodeType)
}
// Swap加载器
if (loaders.includes(nodeData.name)) {
addMenu("↪️ Swap EasyLoader", 'loaders', loaders, nodeType)
}
// Swap预采样器
if (preSampling.includes(nodeData.name)) {
addMenu("↪️ Swap EasyPreSampling", 'preSampling', preSampling, nodeType)
}
// Swap kSampler
if (kSampler.includes(nodeData.name)) {
addMenu("↪️ Swap EasyKSampler", 'preSampling', kSampler, nodeType)
}
// Swap ControlNet
if (controlnet.includes(nodeData.name)) {
addMenu("↪️ Swap EasyControlnet", 'controlnet', controlnet, nodeType)
}
// Swap IPAdapater
if (ipadapter.includes(nodeData.name)) {
addMenu("↪️ Swap EasyAdapater", 'ipadapter', ipadapter, nodeType)
}
// Swap Image
if (imageNode.includes(nodeData.name)) {
addMenu("↪️ Swap LoadImage", 'load_image', imageNode, nodeType)
}
// Swap inpaint
if (inpaint.includes(nodeData.name)) {
addMenu("↪️ Swap InpaintNode", 'inpaint', inpaint, nodeType)
}
}
});

View File

@@ -0,0 +1,788 @@
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el } from "../../../../scripts/ui.js";
import {addPreconnect, addCss} from "../common/utils.js";
const locale = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US'
const customThemeColor = "#236692"
const customThemeColorLight = "#3485bb"
// 增加Slot颜色
const customPipeLineLink = "#7737AA"
const customPipeLineSDXLLink = "#7737AA"
const customIntLink = "#29699C"
const customXYPlotLink = "#74DA5D"
const customLoraStackLink = "#94dccd"
const customXYLink = "#38291f"
var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
if (!customLinkColors["PIPE_LINE"] || !LGraphCanvas.link_type_colors["PIPE_LINE"]) {customLinkColors["PIPE_LINE"] = customPipeLineLink;}
if (!customLinkColors["PIPE_LINE_SDXL"] || !LGraphCanvas.link_type_colors["PIPE_LINE_SDXL"]) {customLinkColors["PIPE_LINE_SDXL"] = customPipeLineSDXLLink;}
if (!customLinkColors["INT"] || !LGraphCanvas.link_type_colors["INT"]) {customLinkColors["INT"] = customIntLink;}
if (!customLinkColors["XYPLOT"] || !LGraphCanvas.link_type_colors["XYPLOT"]) {customLinkColors["XYPLOT"] = customXYPlotLink;}
if (!customLinkColors["X_Y"] || !LGraphCanvas.link_type_colors["X_Y"]) {customLinkColors["X_Y"] = customXYLink;}
if (!customLinkColors["LORA_STACK"] || !LGraphCanvas.link_type_colors["LORA_STACK"]) {customLinkColors["LORA_STACK"] = customLoraStackLink;}
if (!customLinkColors["CONTROL_NET_STACK"] || !LGraphCanvas.link_type_colors["CONTROL_NET_STACK"]) {customLinkColors["CONTROL_NET_STACK"] = customLoraStackLink;}
localStorage.setItem('Comfy.Settings.easyUse.customLinkColors', JSON.stringify(customLinkColors));
// 增加自定义主题
const ui = {
"version": 102,
"id": "obsidian",
"name": "Obsidian",
"colors": {
"node_slot": {
"CLIP": "#FFD500",
"CLIP_VISION": "#A8DADC",
"CLIP_VISION_OUTPUT": "#ad7452",
"CONDITIONING": "#FFA931",
"CONTROL_NET": "#6EE7B7",
"IMAGE": "#64B5F6",
"LATENT": "#FF9CF9",
"MASK": "#81C784",
"MODEL": "#B39DDB",
"STYLE_MODEL": "#C2FFAE",
"VAE": "#FF6E6E",
"TAESD": "#DCC274",
"PIPE_LINE": customPipeLineLink,
"PIPE_LINE_SDXL": customPipeLineSDXLLink,
"INT": customIntLink,
"XYPLOT": customXYPlotLink,
"X_Y": customXYLink
},
"litegraph_base": {
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
"CLEAR_BACKGROUND_COLOR": "#222222",
"NODE_TITLE_COLOR": "rgba(255,255,255,.75)",
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#b8b8b8",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "rgba(0,0,0,.8)",
"NODE_DEFAULT_BGCOLOR": "rgba(22,22,22,.8)",
"NODE_DEFAULT_BOXCOLOR": "rgba(255,255,255,.75)",
"NODE_DEFAULT_SHAPE": "box",
"NODE_BOX_OUTLINE_COLOR": customThemeColor,
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0)",
"DEFAULT_GROUP_FONT": 24,
"WIDGET_BGCOLOR": "#242424",
"WIDGET_OUTLINE_COLOR": "#333",
"WIDGET_TEXT_COLOR": "#a3a3a8",
"WIDGET_SECONDARY_TEXT_COLOR": "#97979c",
"LINK_COLOR": "#9A9",
"EVENT_LINK_COLOR": "#A86",
"CONNECTING_LINK_COLOR": "#AFA"
},
"comfy_base": {
"fg-color": "#fff",
"bg-color": "#242424",
"comfy-menu-bg": "rgba(24,24,24,.9)",
"comfy-input-bg": "#262626",
"input-text": "#ddd",
"descrip-text": "#999",
"drag-text": "#ccc",
"error-text": "#ff4444",
"border-color": "#29292c",
"tr-even-bg-color": "rgba(28,28,28,.9)",
"tr-odd-bg-color": "rgba(19,19,19,.9)"
}
}
}
let custom_theme = null
let control_mode = null
try{
custom_theme = localStorage.getItem('Comfy.Settings.Comfy.CustomColorPalettes') ? JSON.parse(localStorage.getItem('Comfy.Settings.Comfy.CustomColorPalettes')) : {};
}
catch (e) {custom_theme = {}}
try{
const dark_bg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg=="
// 修改自定义主题
if(!custom_theme || !custom_theme.obsidian || !custom_theme.obsidian.version || custom_theme.obsidian.version<ui.version){
custom_theme.obsidian = ui
let ui2 = JSON.parse(JSON.stringify(ui))
ui2.id = 'obsidian_dark'
ui2.name = 'Obsidian Dark'
ui2.colors.litegraph_base.BACKGROUND_IMAGE = dark_bg
ui2.colors.litegraph_base.CLEAR_BACKGROUND_COLOR = '#000'
custom_theme[ui2.id] = ui2
localStorage.setItem('Comfy.Settings.Comfy.CustomColorPalettes', JSON.stringify(custom_theme));
}
let theme_name = localStorage.getItem('Comfy.Settings.Comfy.ColorPalette')
control_mode = localStorage.getItem('Comfy.Settings.Comfy.WidgetControlMode')
// if(control_mode) {
// control_mode = JSON.parse(control_mode)
// if(control_mode == 'before'){
// localStorage['Comfy.Settings.AE.mouseover'] = false
// localStorage['Comfy.Settings.AE.highlight'] = false
// }
// }
// 兼容 ComfyUI Revision: 1887 [235727fe] 以上版本
if(api.storeSettings){
const _settings = await api.getSettings()
let settings = null
// 运行操作设置
if(!control_mode && _settings['Comfy.WidgetControlMode']) {
control_mode = _settings['Comfy.WidgetControlMode']
}else if(!control_mode) control_mode = 'after'
// 主题设置
if(!theme_name && _settings['Comfy.ColorPalette']) {
theme_name = `"${_settings['Comfy.ColorPalette']}"`
localStorage.setItem('Comfy.Settings.Comfy.ColorPalette', theme_name)
}
if(['"custom_obsidian"','"custom_obsidian_dark"'].includes(theme_name)) {
if(!settings) settings = {}
settings["Comfy.ColorPalette"] = JSON.parse(theme_name)
}
if(!_settings || !_settings["Comfy.CustomColorPalettes"] || !_settings["Comfy.CustomColorPalettes"]["obsidian"] || _settings["Comfy.CustomColorPalettes"]["obsidian"]['version']<ui.version){
if(!settings) settings = {}
settings["Comfy.CustomColorPalettes"] = localStorage.getItem('Comfy.Settings.Comfy.CustomColorPalettes') ? JSON.parse(localStorage.getItem('Comfy.Settings.Comfy.CustomColorPalettes')) : {}
await api.storeSettings(settings);
app.ui.settings.load()
}else if(settings){
await api.storeSettings(settings);
}
}
// 判断主题为黑曜石时改变扩展UI
if(['"custom_obsidian"','"custom_obsidian_dark"'].includes(theme_name)){
// 字体文件
addPreconnect("https://fonts.googleapis.com", true)
addCss("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700&amp;family=JetBrains+Mono&amp;display=swap", false)
// 添加easy的主题样式
addCss('css/easy.css')
// canvas
const bgcolor = LGraphCanvas.node_colors.bgcolor;
LGraphCanvas.node_colors = {
red: { color: "#af3535", bgcolor, groupcolor: "#A88" },
brown: { color: "#38291f", bgcolor, groupcolor: "#b06634" },
green: { color: "#346434", bgcolor, groupcolor: "#8A8" },
blue: { color: "#1f1f48", bgcolor, groupcolor: "#88A" },
pale_blue: {color: "#006691", bgcolor, groupcolor: "#3f789e"},
cyan: { color: "#008181", bgcolor, groupcolor: "#8AA" },
purple: { color: "#422342", bgcolor, groupcolor: "#a1309b" },
yellow: { color: "#c09430", bgcolor, groupcolor: "#b58b2a" },
black: { color: "rgba(0,0,0,.8)", bgcolor, groupcolor: "#444" }
};
LiteGraph.NODE_TEXT_SIZE = 13
LiteGraph.DEFAULT_BACKGROUND_IMAGE = ui.colors.litegraph_base.BACKGROUND_IMAGE
LGraphCanvas.prototype.drawNodeShape = function(
node,
ctx,
size,
fgcolor,
bgcolor,
selected,
mouse_over
) {
//bg rect
ctx.strokeStyle = fgcolor;
ctx.fillStyle = bgcolor;
var title_height = LiteGraph.NODE_TITLE_HEIGHT;
var low_quality = this.ds.scale < 0.5;
//render node area depending on shape
var shape =
node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
var title_mode = node.constructor.title_mode;
var render_title = true;
if (title_mode == LiteGraph.TRANSPARENT_TITLE || title_mode == LiteGraph.NO_TITLE) {
render_title = false;
} else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) {
render_title = true;
}
var area = new Float32Array(4);
area[0] = 0; //x
area[1] = render_title ? -title_height : 0; //y
area[2] = size[0] + 1; //w
area[3] = render_title ? size[1] + title_height : size[1]; //h
var old_alpha = ctx.globalAlpha;
//full node shape
// if(node.flags.collapsed)
{
ctx.lineWidth = 1;
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE || low_quality) {
ctx.fillRect(area[0], area[1], area[2], area[3]);
} else if (
shape == LiteGraph.ROUND_SHAPE ||
shape == LiteGraph.CARD_SHAPE
) {
ctx.roundRect(
area[0],
area[1],
area[2],
area[3],
shape == LiteGraph.CARD_SHAPE ? [this.round_radius,this.round_radius,0,0] : [this.round_radius]
);
} else if (shape == LiteGraph.CIRCLE_SHAPE) {
ctx.arc(
size[0] * 0.5,
size[1] * 0.5,
size[0] * 0.5,
0,
Math.PI * 2
);
}
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.stroke();
ctx.strokeStyle = fgcolor;
ctx.fill();
//separator
if(!node.flags.collapsed && render_title)
{
ctx.shadowColor = "transparent";
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.fillRect(0, -1, area[2], 2);
}
}
ctx.shadowColor = "transparent";
if (node.onDrawBackground) {
node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse );
}
//title bg (remember, it is rendered ABOVE the node)
if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) {
//title bar
if (node.onDrawTitleBar) {
node.onDrawTitleBar( ctx, title_height, size, this.ds.scale, fgcolor );
} else if (
title_mode != LiteGraph.TRANSPARENT_TITLE &&
(node.constructor.title_color || this.render_title_colored)
) {
var title_color = node.constructor.title_color || fgcolor;
if (node.flags.collapsed) {
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR;
}
//* gradient test
if (this.use_gradients) {
var grad = LGraphCanvas.gradients[title_color];
if (!grad) {
grad = LGraphCanvas.gradients[ title_color ] = ctx.createLinearGradient(0, 0, 400, 0);
grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException
grad.addColorStop(1, "#000");
}
ctx.fillStyle = grad;
} else {
ctx.fillStyle = title_color;
}
//ctx.globalAlpha = 0.5 * old_alpha;
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE || low_quality) {
ctx.rect(0, -title_height, size[0] + 1, title_height);
} else if ( shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE ) {
ctx.roundRect(
0,
-title_height,
size[0] + 1,
title_height,
node.flags.collapsed ? [this.round_radius] : [this.round_radius,this.round_radius,0,0]
);
}
ctx.fill();
ctx.shadowColor = "transparent";
}
var colState = false;
if (LiteGraph.node_box_coloured_by_mode){
if(LiteGraph.NODE_MODES_COLORS[node.mode]){
colState = LiteGraph.NODE_MODES_COLORS[node.mode];
}
}
if (LiteGraph.node_box_coloured_when_on){
colState = node.action_triggered ? "#FFF" : (node.execute_triggered ? "#AAA" : colState);
}
//title box
var box_size = 10;
if (node.onDrawTitleBox) {
node.onDrawTitleBox(ctx, title_height, size, this.ds.scale);
} else if (
shape == LiteGraph.ROUND_SHAPE ||
shape == LiteGraph.CIRCLE_SHAPE ||
shape == LiteGraph.CARD_SHAPE
) {
if (low_quality) {
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(
title_height * 0.5,
title_height * -0.5,
box_size * 0.5 + 1,
0,
Math.PI * 2
);
ctx.fill();
}
// BOX_TITLE_ICON
ctx.fillStyle = selected ? LiteGraph.NODE_SELECTED_TITLE_COLOR : (node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR);
ctx.beginPath();
ctx.fillRect(10,0-box_size-1,box_size * 1.15,box_size * 0.15);
ctx.fillRect(10,0-box_size*1.5-1,box_size * 1.15,box_size * 0.15);
ctx.fillRect(10,0-box_size*2-1,box_size * 1.15,box_size * 0.15);
} else {
if (low_quality) {
ctx.fillStyle = "black";
ctx.fillRect(
(title_height - box_size) * 0.5 - 1,
(title_height + box_size) * -0.5 - 1,
box_size + 2,
box_size + 2
);
}
ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR;
ctx.fillRect(
(title_height - box_size) * 0.5,
(title_height + box_size) * -0.5,
box_size,
box_size
);
}
ctx.globalAlpha = old_alpha;
//title text
if (node.onDrawTitleText) {
node.onDrawTitleText(
ctx,
title_height,
size,
this.ds.scale,
this.title_text_font,
selected
);
}
if (!low_quality) {
ctx.font = this.title_text_font;
var title = String(node.getTitle());
if (title) {
if (selected) {
ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR;
} else {
ctx.fillStyle =
node.constructor.title_text_color ||
this.node_title_color;
}
if (node.flags.collapsed) {
ctx.textAlign = "left";
var measure = ctx.measureText(title);
ctx.fillText(
title.substr(0,20), //avoid urls too long
title_height,// + measure.width * 0.5,
LiteGraph.NODE_TITLE_TEXT_Y - title_height
);
ctx.textAlign = "left";
} else {
ctx.textAlign = "left";
ctx.fillText(
title,
title_height,
LiteGraph.NODE_TITLE_TEXT_Y - title_height
);
}
}
}
//subgraph box
if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) {
var w = LiteGraph.NODE_TITLE_HEIGHT;
var x = node.size[0] - w;
var over = LiteGraph.isInsideRectangle( this.graph_mouse[0] - node.pos[0], this.graph_mouse[1] - node.pos[1], x+2, -w+2, w-4, w-4 );
ctx.fillStyle = over ? "#888" : "#555";
if( shape == LiteGraph.BOX_SHAPE || low_quality)
ctx.fillRect(x+2, -w+2, w-4, w-4);
else
{
ctx.beginPath();
ctx.roundRect(x+2, -w+2, w-4, w-4,[4]);
ctx.fill();
}
ctx.fillStyle = "#333";
ctx.beginPath();
ctx.moveTo(x + w * 0.2, -w * 0.6);
ctx.lineTo(x + w * 0.8, -w * 0.6);
ctx.lineTo(x + w * 0.5, -w * 0.3);
ctx.fill();
}
//custom title render
if (node.onDrawTitle) {
node.onDrawTitle(ctx);
}
}
//render selection marker
if (selected) {
if (node.onBounding) {
node.onBounding(area);
}
if (title_mode == LiteGraph.TRANSPARENT_TITLE) {
area[1] -= title_height;
area[3] += title_height;
}
ctx.lineWidth = 2;
ctx.globalAlpha = 0.8;
ctx.beginPath();
// var out_a = -6,out_b = 12,scale = 2
var out_a = 0, out_b = 0, scale = 1
if (shape == LiteGraph.BOX_SHAPE) {
ctx.rect(
out_a + area[0],
out_a + area[1],
out_b + area[2],
out_b + area[3]
);
} else if (
shape == LiteGraph.ROUND_SHAPE ||
(shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)
) {
ctx.roundRect(
out_a + area[0],
out_a + area[1],
out_b + area[2],
out_b + area[3],
[this.round_radius * scale]
);
} else if (shape == LiteGraph.CARD_SHAPE) {
ctx.roundRect(
out_a + area[0],
out_a + area[1],
out_b + area[2],
out_b + area[3],
[this.round_radius * scale,scale,this.round_radius * scale,scale]
);
} else if (shape == LiteGraph.CIRCLE_SHAPE) {
ctx.arc(
size[0] * 0.5,
size[1] * 0.5,
size[0] * 0.5 + 6,
0,
Math.PI * 2
);
}
ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR;
ctx.stroke();
ctx.strokeStyle = fgcolor;
ctx.globalAlpha = 1;
}
// these counter helps in conditioning drawing based on if the node has been executed or an action occurred
if (node.execute_triggered>0) node.execute_triggered--;
if (node.action_triggered>0) node.action_triggered--;
};
LGraphCanvas.prototype.drawNodeWidgets = function(
node,
posY,
ctx,
active_widget
) {
if (!node.widgets || !node.widgets.length) {
return 0;
}
var width = node.size[0];
var widgets = node.widgets;
posY += 2;
var H = LiteGraph.NODE_WIDGET_HEIGHT;
var show_text = this.ds.scale > 0.5;
ctx.save();
ctx.globalAlpha = this.editor_alpha;
var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR;
var background_color = LiteGraph.WIDGET_BGCOLOR;
var text_color = LiteGraph.WIDGET_TEXT_COLOR;
var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
var margin = 12;
for (var i = 0; i < widgets.length; ++i) {
var w = widgets[i];
var y = posY;
if (w.y) {
y = w.y;
}
w.last_y = y;
ctx.strokeStyle = outline_color;
ctx.fillStyle = "#222";
ctx.textAlign = "left";
ctx.lineWidth = 1;
if(w.disabled)
ctx.globalAlpha *= 0.5;
var widget_width = w.width || width;
switch (w.type) {
case "button":
ctx.font = "10px Inter"
ctx.fillStyle = background_color;
if (w.clicked) {
ctx.fillStyle = "#AAA";
w.clicked = false;
this.dirty_canvas = true;
}
ctx.beginPath();
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25]);
ctx.fill();
if(show_text && !w.disabled)
ctx.stroke();
if (show_text) {
ctx.textAlign = "center";
ctx.fillStyle = text_color;
ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7);
}
break;
case "toggle":
ctx.font = "10px Inter"
ctx.textAlign = "left";
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
if (show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25]);
else
ctx.rect(margin, y, widget_width - margin * 2, H );
ctx.fill();
if(show_text && !w.disabled)
ctx.stroke();
ctx.fillStyle = w.value ? customThemeColor : "#333";
ctx.beginPath();
ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.25, 0, Math.PI * 2 );
ctx.fill();
if (show_text) {
ctx.fillStyle = secondary_text_color;
const label = w.label || w.name;
if (label != null) {
ctx.fillText(label, margin * 1.6, y + H * 0.7);
}
ctx.font = "10px Inter"
ctx.fillStyle = w.value ? text_color : secondary_text_color;
ctx.textAlign = "right";
ctx.fillText(
w.value
? w.options.on || "true"
: w.options.off || "false",
widget_width - 35,
y + H * 0.7
);
}
break;
case "slider":
ctx.font = "10px Inter"
ctx.fillStyle = background_color;
ctx.strokeStyle = outline_color;
ctx.beginPath();
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H*0.25]);
ctx.fill();
ctx.stroke()
var range = w.options.max - w.options.min;
var nvalue = (w.value - w.options.min) / range;
if(nvalue < 0.0) nvalue = 0.0;
if(nvalue > 1.0) nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#333" : customThemeColor);
ctx.beginPath();
ctx.roundRect(margin, y, nvalue * (widget_width - margin * 2), H, [H*0.25]);
ctx.fill();
if (w.marker) {
var marker_nvalue = (w.marker - w.options.min) / range;
if(marker_nvalue < 0.0) marker_nvalue = 0.0;
if(marker_nvalue > 1.0) marker_nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9";
ctx.roundRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H , [H * 0.25] );
}
if (show_text) {
ctx.textAlign = "center";
ctx.fillStyle = text_color;
var text = (w.label || w.name) + ": " + (Number(w.value).toFixed(w.options.precision != null ? w.options.precision : 3)).toString()
ctx.fillText(
text,
widget_width * 0.5,
y + H * 0.7
);
}
break;
case "number":
case "combo":
ctx.textAlign = "left";
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
if(show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25] );
else
ctx.rect(margin, y, widget_width - margin * 2, H );
ctx.fill();
if (show_text) {
if(!w.disabled)
ctx.stroke();
ctx.fillStyle = text_color;
if(!w.disabled)
{
ctx.beginPath();
ctx.moveTo(margin + 12, y + 6.5);
ctx.lineTo(margin + 6, y + H * 0.5);
ctx.lineTo(margin + 12, y + H - 6.5);
ctx.fill();
ctx.beginPath();
ctx.moveTo(widget_width - margin - 12, y + 6.5);
ctx.lineTo(widget_width - margin - 6, y + H * 0.5);
ctx.lineTo(widget_width - margin - 12, y + H - 6.5);
ctx.fill();
}
ctx.fillStyle = secondary_text_color;
ctx.font = "10px Inter"
ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7);
ctx.fillStyle = text_color;
ctx.textAlign = "right";
var rightDistance = 6
if (w.type == "number") {
ctx.font = "10px Inter,JetBrains Mono,monospace"
ctx.fillText(
Number(w.value).toFixed(
w.options.precision !== undefined
? w.options.precision
: 3
),
widget_width - margin * 2 - rightDistance,
y + H * 0.7
);
} else {
var v = w.value;
if( w.options.values )
{
var values = w.options.values;
if( values.constructor === Function )
values = values();
if(values && values.constructor !== Array)
v = values[ w.value ];
}
ctx.fillText(
v,
widget_width - margin * 2 - rightDistance,
y + H * 0.7
);
}
}
break;
case "string":
case "text":
ctx.textAlign = "left";
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
if (show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25]);
else
ctx.rect( margin, y, widget_width - margin * 2, H );
ctx.fill();
if (show_text) {
if(!w.disabled)
ctx.stroke();
ctx.save();
ctx.beginPath();
ctx.rect(margin, y, widget_width - margin * 2, H);
ctx.clip();
//ctx.stroke();
ctx.fillStyle = secondary_text_color;
const label = w.label || w.name;
ctx.font = "10px Inter"
if (label != null) {
ctx.fillText(label, margin * 2, y + H * 0.7);
}
ctx.fillStyle = text_color;
ctx.textAlign = "right";
ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max
ctx.restore();
}
break;
default:
if (w.draw) {
w.draw(ctx, node, widget_width, y, H);
}
break;
}
posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4;
ctx.globalAlpha = this.editor_alpha;
}
ctx.restore();
ctx.textAlign = "left";
};
}
}catch(e){
console.error(e)
}
function updateControlWidgetLabel(widget, controlValueRunBefore=false) {
let replacement = "after";
let find = "before";
if (controlValueRunBefore) {
[find, replacement] = [replacement, find]
}
widget.label = (widget.label ?? widget.name).replace(find, replacement);
widget.name = widget.label;
}
// 节点颜色
const COLOR_THEMES = LGraphCanvas.node_colors
const NODE_COLORS = {
"easy positive":"green",
"easy negative":"red",
"easy promptList":"cyan",
"easy promptLine":"cyan",
"easy promptConcat":"cyan",
"easy promptReplace":"cyan",
"easy XYInputs: Seeds++ Batch": customXYLink,
"easy XYInputs: ModelMergeBlocks": customXYLink,
'easy textSwitch': "pale_blue"
}
function setNodeColors(node, theme) {
if (!theme) {return;}
if(theme.color) node.color = theme.color;
if(theme.bgcolor) node.bgcolor = theme.bgcolor;
}
app.registerExtension({
name: "comfy.easyUse.interface",
setup() {
Object.assign(app.canvas.default_connection_color_byType, customLinkColors);
Object.assign(LGraphCanvas.link_type_colors, customLinkColors);
},
async nodeCreated(node) {
if (NODE_COLORS.hasOwnProperty(node.comfyClass)) {
const colorKey = NODE_COLORS[node.comfyClass]
const theme = COLOR_THEMES[colorKey];
setNodeColors(node, theme);
}
// 修复官方bug: 应该初始化修改节点的control_mode name
if(control_mode && control_mode == 'before'){
const controlValueRunBefore = control_mode == 'before'
if(node.widgets && node.widgets.length>0) {
for (const w of node.widgets) {
if (['control_before_generate', 'control_after_generate'].includes(w.name)) {
await updateControlWidgetLabel(w, controlValueRunBefore);
if (w.linkedWidgets) {
for (const l of w.linkedWidgets) {
await updateControlWidgetLabel(l, controlValueRunBefore);
}
}
}
}
}
}
},
})

View File

@@ -0,0 +1,250 @@
// 1.0.2
import { app } from "../../../../scripts/app.js";
import { GroupNodeConfig } from "../../../../extensions/core/groupNode.js";
import { api } from "../../../../scripts/api.js";
import { $t } from "../common/i18n.js"
const nodeTemplateShortcutId = "Comfy.EasyUse.NodeTemplateShortcut"
const processBarId = "Comfy.EasyUse.queueProcessBar"
let enableNodeTemplateShortcut = true
let enableQueueProcess = false
export function addNodeTemplateShortcutSetting(app) {
app.ui.settings.addSetting({
id: nodeTemplateShortcutId,
name: $t("Enable ALT+1~9 to paste nodes from nodes template (ComfyUI-Easy-Use)"),
type: "boolean",
defaultValue: enableNodeTemplateShortcut,
onChange(value) {
enableNodeTemplateShortcut = !!value;
},
});
}
export function addQueueProcessSetting(app) {
app.ui.settings.addSetting({
id: processBarId,
name: $t("Enable process bar in queue button (ComfyUI-Easy-Use)"),
type: "boolean",
defaultValue: enableQueueProcess,
onChange(value) {
enableQueueProcess = !!value;
},
});
}
const getEnableNodeTemplateShortcut = _ => app.ui.settings.getSettingValue(nodeTemplateShortcutId, true)
const getQueueProcessSetting = _ => app.ui.settings.getSettingValue(processBarId, false)
function loadTemplate(){
return localStorage['Comfy.NodeTemplates'] ? JSON.parse(localStorage['Comfy.NodeTemplates']) : null
}
const clipboardAction = async (cb) => {
const old = localStorage.getItem("litegrapheditor_clipboard");
await cb();
localStorage.setItem("litegrapheditor_clipboard", old);
};
async function addTemplateToCanvas(t){
const data = JSON.parse(t.data);
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
localStorage.setItem("litegrapheditor_clipboard", t.data);
app.canvas.pasteFromClipboard();
}
app.registerExtension({
name: 'comfy.easyUse.quick',
init() {
const keybindListener = async function (event) {
let modifierPressed = event.altKey;
const isEnabled = getEnableNodeTemplateShortcut()
if(isEnabled){
const mac_alt_nums = ['¡','™','£','¢','∞','§','¶','•','ª']
const nums = ['1','2','3','4','5','6','7','8','9']
let key = event.key
if(mac_alt_nums.includes(key)){
const idx = mac_alt_nums.findIndex(cate=> cate == key)
key = nums[idx]
modifierPressed = true
}
if(['1','2','3','4','5','6','7','8','9'].includes(key) && modifierPressed) {
const template = loadTemplate()
const idx = parseInt(key) - 1
if (template && template[idx]) {
let t = template[idx]
try{
let data = JSON.parse(t.data)
data.title = t.name
t.data = JSON.stringify(data)
clipboardAction(_ => {
addTemplateToCanvas(t)
})
}catch (e){
console.error(e)
}
}
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
}
}
}
window.addEventListener("keydown", keybindListener, true);
},
setup(app) {
addNodeTemplateShortcutSetting(app)
addQueueProcessSetting(app)
registerListeners()
}
});
const registerListeners = () => {
const queue_button = document.getElementById("queue-button")
const old_queue_button_text = queue_button.innerText
api.addEventListener('progress', ({
detail,
}) => {
const isEnabled = getQueueProcessSetting()
if(isEnabled){
const {
value, max, node,
} = detail;
const progress = Math.floor((value / max) * 100);
// console.log(progress)
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
queue_button.innerText = progress ==0 || progress == 100 ? old_queue_button_text : " "
const width = progress ==0 || progress == 100 ? '0%' : progress.toString() + '%'
let bar = document.createElement("div")
queue_button.setAttribute('data-attr', progress ==0 || progress == 100 ? "" : progress.toString() + '%')
document.documentElement.style.setProperty('--process-bar-width', width)
}
}
}, false);
api.addEventListener('status', ({
detail,
}) => {
const queueRemaining = detail?.exec_info.queue_remaining;
if(queueRemaining === 0){
let queue_button = document.getElementById("queue-button")
queue_button.innerText = old_queue_button_text
queue_button.setAttribute('data-attr', "")
document.documentElement.style.setProperty('--process-bar-width', '0%')
}
}, false);
};
// 修改粘贴指令
LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) {
// if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior
if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
return;
}
var data = localStorage.getItem("litegrapheditor_clipboard");
if (!data) {
return;
}
this.graph.beforeChange();
//create nodes
var clipboard_info = JSON.parse(data);
// calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos
var posMin = false;
var posMinIndexes = false;
for (var i = 0; i < clipboard_info.nodes.length; ++i) {
if (posMin){
if(posMin[0]>clipboard_info.nodes[i].pos[0]){
posMin[0] = clipboard_info.nodes[i].pos[0];
posMinIndexes[0] = i;
}
if(posMin[1]>clipboard_info.nodes[i].pos[1]){
posMin[1] = clipboard_info.nodes[i].pos[1];
posMinIndexes[1] = i;
}
}
else{
posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]];
posMinIndexes = [i, i];
}
}
var nodes = [];
var left_arr = [], right_arr = [], top_arr =[], bottom_arr =[];
for (var i = 0; i < clipboard_info.nodes.length; ++i) {
var node_data = clipboard_info.nodes[i];
var node = LiteGraph.createNode(node_data.type);
if (node) {
node.configure(node_data);
//paste in last known mouse position
node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5;
node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5;
left_arr.push(node.pos[0])
right_arr.push(node.pos[0] + node.size[0])
top_arr.push(node.pos[1])
bottom_arr.push(node.pos[1] + node.size[1])
this.graph.add(node,{doProcessChange:false});
nodes.push(node);
}
}
if(clipboard_info.title){
var l = Math.min(...left_arr) - 15;
var r = Math.max(...right_arr) - this.graph_mouse[0] + 30;
var t = Math.min(...top_arr) - 100;
var b = Math.max(...bottom_arr) - this.graph_mouse[1] + 130;
// create group
const groups = [
{
"title": clipboard_info.title,
"bounding": [
l,
t,
r,
b
],
"color": "#3f789e",
"font_size": 24,
"locked": false
}
]
for (var i = 0; i < groups.length; ++i) {
var group = new LiteGraph.LGraphGroup();
group.configure(groups[i]);
this.graph.add(group);
}
}
//create links
for (var i = 0; i < clipboard_info.links.length; ++i) {
var link_info = clipboard_info.links[i];
var origin_node;
var origin_node_relative_id = link_info[0];
if (origin_node_relative_id != null) {
origin_node = nodes[origin_node_relative_id];
} else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
var origin_node_id = link_info[4];
if (origin_node_id) {
origin_node = this.graph.getNodeById(origin_node_id);
}
}
var target_node = nodes[link_info[2]];
if( origin_node && target_node )
origin_node.connect(link_info[1], target_node, link_info[3]);
else
console.warn("Warning, nodes missing on pasting");
}
this.selectNodes(nodes);
this.graph.afterChange();
};

View File

@@ -0,0 +1,36 @@
import { app } from "../../../../scripts/app.js";
import { applyTextReplacements } from "../../../../scripts/utils.js";
const extraNodes = ["easy imageSave", "easy fullkSampler", "easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo","easy detailerFix"]
app.registerExtension({
name: "Comfy.Easy.SaveImageExtraOutput",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (extraNodes.includes(nodeData.name)) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const widget = this.widgets.find((w) => w.name === "filename_prefix" || w.name === 'save_prefix');
widget.serializeValue = () => {
return applyTextReplacements(app, widget.value);
};
return r;
};
} else {
// When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty("Node name for S&R", this.constructor.type, "string");
}
return r;
};
}
},
});

View File

@@ -0,0 +1,136 @@
import {app} from "../../../../scripts/app.js";
import {$el} from "../../../../scripts/ui.js";
import {$t} from "../common/i18n.js";
import {findWidgetByName, toggleWidget} from "../common/utils.js";
const tags = {
"selfie_multiclass_256x256": ["Background", "Hair", "Body", "Face", "Clothes", "Others",],
"human_parsing_lip":["Background","Hat","Hair","Glove","Sunglasses","Upper-clothes","Dress","Coat","Socks","Pants","Jumpsuits","Scarf","Skirt","Face","Left-arm","Right-arm","Left-leg","Right-leg","Left-shoe","Right-shoe"],
}
function getTagList(tags) {
let rlist=[]
tags.forEach((k,i) => {
rlist.push($el(
"label.easyuse-prompt-styles-tag",
{
dataset: {
tag: i,
name: $t(k),
index: i
},
$: (el) => {
el.children[0].onclick = () => {
el.classList.toggle("easyuse-prompt-styles-tag-selected");
};
},
},
[
$el("input",{
type: 'checkbox',
name: i
}),
$el("span",{
textContent: $t(k),
})
]
))
});
return rlist
}
app.registerExtension({
name: 'comfy.easyUse.seg',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name == 'easy humanSegmentation') {
// 创建时
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined;
const method = this.widgets.findIndex((w) => w.name == 'method');
const list = $el("ul.easyuse-prompt-styles-list.no-top", []);
let method_values = ''
this.setProperty("values", [])
let selector = this.addDOMWidget('mask_components',"btn",$el('div.easyuse-prompt-styles',[list]))
Object.defineProperty(this.widgets[method],'value',{
set:(value)=>{
method_values = value
if(method_values){
selector.element.children[0].innerHTML = ''
if(method_values == 'selfie_multiclass_256x256'){
toggleWidget(this, findWidgetByName(this, 'confidence'), true)
this.setSize([300, 260]);
}else{
toggleWidget(this, findWidgetByName(this, 'confidence'))
this.setSize([300, 500]);
}
let list = getTagList(tags[method_values]);
selector.element.children[0].append(...list)
}
},
get: () => {
return method_values
}
})
let mask_select_values = ''
Object.defineProperty(selector, "value", {
set: (value) => {
setTimeout(_=>{
selector.element.children[0].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
let arr = value.split(',')
if (arr.includes(el.dataset.tag)) {
el.classList.add("easyuse-prompt-styles-tag-selected");
el.children[0].checked = true
}
})
},100)
},
get: () => {
selector.element.children[0].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
if(el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")>=0){
if(!this.properties["values"].includes(el.dataset.tag)){
this.properties["values"].push(el.dataset.tag);
}
}else{
if(this.properties["values"].includes(el.dataset.tag)){
this.properties["values"]= this.properties["values"].filter(v=>v!=el.dataset.tag);
}
}
});
mask_select_values = this.properties["values"].join(',');
return mask_select_values;
}
});
let old_values = ''
let mask_lists_dom = selector.element.children[0]
// 初始化
setTimeout(_=>{
if(!method_values) {
method_values = 'selfie_multiclass_256x256'
selector.element.children[0].innerHTML = ''
// 重新排序
let list = getTagList(tags[method_values]);
selector.element.children[0].append(...list)
}
if(method_values == 'selfie_multiclass_256x256'){
toggleWidget(this, findWidgetByName(this, 'confidence'), true)
this.setSize([300, 260]);
}else{
toggleWidget(this, findWidgetByName(this, 'confidence'))
this.setSize([300, 500]);
}
},1)
return onNodeCreated;
}
}
}
})

View File

@@ -0,0 +1,296 @@
// 1.0.3
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el } from "../../../../scripts/ui.js";
import { $t } from "../common/i18n.js";
// 获取风格列表
let styles_list_cache = {}
let styles_image_cache = {}
async function getStylesList(name){
if(styles_list_cache[name]) return styles_list_cache[name]
else{
const resp = await api.fetchApi(`/easyuse/prompt/styles?name=${name}`);
if (resp.status === 200) {
let data = await resp.json();
styles_list_cache[name] = data;
return data;
}
return undefined;
}
}
async function getStylesImage(name, styles_name){
if(!styles_image_cache[styles_name]) styles_image_cache[styles_name] = {}
if(styles_image_cache[styles_name][name]) return styles_image_cache[styles_name][name]
else{
const resp = await api.fetchApi(`/easyuse/prompt/styles/image?name=${name}&styles_name=${styles_name}`);
if (resp.status === 200) {
const text = await resp.text()
if(text.startsWith('http')){
styles_image_cache[styles_name][name] = text
return text
}
const url = `/easyuse/prompt/styles/image?name=${name}&styles_name=${styles_name}`
styles_image_cache[styles_name][name] = url
return url
}
return undefined;
}
}
function getTagList(tags, styleName, language='en-US') {
let rlist=[]
tags.forEach((k,i) => {
rlist.push($el(
"label.easyuse-prompt-styles-tag",
{
dataset: {
tag: k['name'],
name: language == 'zh-CN' && k['name_cn'] ? k['name_cn'] : k['name'],
imgName: k['imgName'],
index: i
},
$: (el) => {
el.children[0].onclick = () => {
el.classList.toggle("easyuse-prompt-styles-tag-selected");
};
el.onmousemove = (e) => {
displayImage(el.dataset.imgName, styleName, e)
};
el.onmouseout = () => {
hiddenImage()
};
el.onmouseover = (e) => {
displayImage(el.dataset.imgName, styleName)
};
},
},
[
$el("input",{
type: 'checkbox',
name: k['name']
}),
$el("span",{
textContent: language == 'zh-CN' && k['name_cn'] ? k['name_cn'] : k['name'],
})
]
))
});
return rlist
}
const foocus_base_path = "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/"
const empty_img = "data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QNLaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoMjAyMzA5MDUubS4yMzE2IDk3OWM4NmQpICAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RjA3NEU1QzNCNUJBMTFFRUExMUVDNkZDRjI0NzlBN0QiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RjA3NEU1QzRCNUJBMTFFRUExMUVDNkZDRjI0NzlBN0QiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGMDc0RTVDMUI1QkExMUVFQTExRUM2RkNGMjQ3OUE3RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGMDc0RTVDMkI1QkExMUVFQTExRUM2RkNGMjQ3OUE3RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx8BBwcHDQwNGBAQGBoVERUaHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH//AABEIAIAAgAMBEQACEQEDEQH/xACLAAEAAgMBAQEAAAAAAAAAAAAABAUCAwYBBwgBAQADAQEBAAAAAAAAAAAAAAABAgMEBQYQAAEEAgECAwUHAwUAAAAAAAEAAgMEEQUhEgYxEwdBYSIyFFFxgVJyIxWRoTOxwdFiJBEBAAICAQQBBAIDAAAAAAAAAAECEQMxIUESBBOB0SIyUXGCIwX/2gAMAwEAAhEDEQA/AP1SgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICDXJYgj+d4afsVopM8KWvEcy8it1pXdMcjXO/Lnn+im2u0cwV2VniW1UXEBAQEBAQEBAQEBAQRNlc+mgyDh7zhv+5WunX5Sw37fCHM2dh48r06ank7N6rn2Ja7qa4hw5BBwQV010uK+/DsO29v/J68SOI86Jxjl95HIP4gryPc0fHfHaXu+j7Py68zzHSVquV2iAgICAgICAgICDyTr6HdHz4PTnwypjnqic46OauNbY6mGX99p+L8w9xaeV6OufHt0eXtr59M9VFb194E9LmuH3kf6rv17avO2ets7YVcuuuk/uOa3PgBlxP4BdMbq9nLPqbJ5xDbSM9azFXpyujuSO+Bo5kcf0NPyj25We2YtEzaPxdfr6519Kz+UvqEIlELBKQZQ0eYRwC7HOPxXzVsZ6cPpK5x15ZKEiAgICAgICAgICCNc1tG40CzA2XHg4j4h9zhyFpr22p+s4Z7NNL/ALRlTX+1dVFBJOJrcTI2lxZHYcBx+sldWv3bzOMVn6fZy39OkRnNo+v3aoOx9JOxks8tqwHDPS+1IW8+IzGWZVrf9DZHSMR/j9yvo656zMz9V1rdLqdYwsoVIqwd87mNAc79Tvmd+JXJt332ftMy6temlP1jCasmggICAgICAgICAgwlmiib1SPDB7zhWrWZ4VtaI5QXb2l5ojYHvLjjIGB/dbR61sZlhPtVziFb3PYdd0luCvAZbXludVZ1huZQPgyTx4/atvWj4rxaZ6d/6Ye1/t1zSI6zx/bzti5YqaOpBeg8u41n/oa14cA4ccH7lPs1jZebVn8eyPUtOrXFbR+XdYx9xa90pjeXROaSCXDj+oysZ9S+Mx1bR7uvOJ6LGOWKVgfG8PafAtOQueazHLqraJjMMlCRAQEBAQEBAQRLNp4HTFx/2/4WtKR3Y32T2Udl8j3knk/aeSu6kREPPvaZlpY3DmyY8DyrzPZWv8tkvmFv7bg12RyR1DGeeMj2KnjE9JaeUx1hi1sgaet/U7JIOMcE8Dj7FMREcK2zPKMasr5XO6fmOVt5xEOadVplYU45IAOhxa72kLm2TFuXXqrNeF1WtlwDZeHfmHguO+vHDupszylLJsICAgICAg8cMjCQiYR5IVpFmc1Q5qLXHPgfbhbV2MLaYlqNQAYA4V/kV+PDA1fcp81fjYurtYMu4CmLZRNYhtZWBAI8CqzdaKN8df3LObtIokxwe5ZzZrFUloIGFnLWHqhIgICAgICAgxMbSpyjDAwAq3kr4MTWCnzR4MX02PGHDISNmETqieWba7QABwB4KJumKNgjaFXK0VZYChYQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHzvuv1G7k1W9s6/Xamtaq15oaonmnsCR008HntaI4K8/s4HOeEGXZXqTud7uqtG7r6kNa5HdMU9aaw9zZde+FkrHsnr1+M2MZBPIKDRe9cO2K2mjs/V0m7X61lWzq32W+ZFEbfkSSO4B+GL9zw4QWm99TqFVmjsaSu7fUtxeNM2aTmSMBbHI9zWHqHVJlnDTxjPKCJL6sea502t1D7Ouhr0rNqxNM2CSNuwnkgjAi6ZOotdEc/Egibf1j/j+7JNL9DWdWg84TWn2ywtdFKyMZb5Tg0nLyG55x48IJ3bXqe/ea/a26dFtyTXtldDUqyOdNL5VqaDHS5gwXRxMe3xz1Y9iDKP1Sa7uefUnR7TyYqUVoEU5jY6pJZIz1RY4ZiMYd7TkexBA749Wr2gtCKlrIpGs17NjK29LLWmPmMsyiFkbIZsPEdKQu6y0eAQWdD1E2L93W1tzRyCDY3paev2NaxVlhIjidMfMb5vmse1kbi9pZ7MeKDt0BAQEBAQfEPU+lFY2++q2K1uSSezTnrReVsTTmiZVYHOd9LVuQyubIwANkbxz4FA7FsQ0NrrLNXX7N0eo1+3darGDYPjb5j6prxVRajjDetsRAjj4yM4CDre2uxO7q2hqtm7nua6w9rp5tfXgoSxwyTOMr42PlrPe4Nc8jJJQRDb3Oz1fYFrcV7As0mu3u7nbWkBZ9LSfG5nlxs/yySWRiNozwcBBx9EXadGTXz62+LG41+jZS6adhzS6vfnlkEjgzEZax7T8ePFBu3nbPdUXqJZsw6S5cqbCW1YdIY2lxhhfEGMjfHtoG9HxucwPEZy4/A7kMC87aq2Kmv7mdvxuqGmklFjUU4G2Yp21rdyW00t+kJkFl88pY9vDgwNDvEoK9np73FBcHdkrt2+rZd5FjQx7O0b8WvbzDKZhN1SSse573QdeAHkN+Ichj3p2rBvZq9vUnY2tcNQPqpZYZpJ44GxXqzHdVlzZZpib73mLHViI85c1BZ6OpsIe/6/XSuntevdsz6+8+pI0/yM1dtWVr2Z644P8rmyuj6S53jxkh9aQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBB/9k="
async function displayImage(imgName, styleName) {
var e = event || window.event;
var img = document.getElementById("show_image_id");
var pxy= img.parentElement.getBoundingClientRect();
if(imgName) {
const url = await getStylesImage(imgName, styleName)
img.src = url
img.onerror = _ =>{
img.src = empty_img
}
}
var scale = app?.canvas?.ds?.scale || 1;
var x = (e.pageX-pxy.x-100)/scale;
var y = (e.pageY-pxy.y+25)/scale;
img.style.left = x+"px";
img.style.top = y+"px";
img.style.display = "block";
img.style.borderRadius = "10px";
img.style.borderColor = "var(--fg-color)"
img.style.borderWidth = "1px";
img.style.borderStyle = "solid";
}
function hiddenImage(){ //theEvent用来传入事件Firefox的方式
var img = document.getElementById('show_image_id');
img.style.display = "none";
}
// StylePromptSelector
app.registerExtension({
name: 'comfy.easyUse.styleSelector',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData.name == 'easy stylesSelector'){
// 创建时
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined;
const styles_id = this.widgets.findIndex((w) => w.name == 'styles');
const language = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US'
const list = $el("ul.easyuse-prompt-styles-list",[]);
let styles_values = ''
this.setProperty("values", [])
let selector = this.addDOMWidget('select_styles',"btn",$el('div.easyuse-prompt-styles',[$el('div.tools', [
$el('button.delete',{
textContent: $t('Empty All'),
style:{},
onclick:()=>{
selector.element.children[0].querySelectorAll(".search").forEach(el=>{
el.value = ''
})
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag-selected").forEach(el => {
el.classList.remove("easyuse-prompt-styles-tag-selected");
el.children[0].checked = false
})
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
el.classList.remove('hide')
})
this.setProperty("values", [])
}}
),
$el('textarea.search',{
dir:"ltr",
style:{"overflow-y": "scroll"},
rows:1,
placeholder:$t("🔎 Type here to search styles ..."),
oninput:(e)=>{
let value = e.target.value
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
const name = el.dataset.name.toLowerCase()
const tag = el.dataset.tag.toLowerCase()
const lower_value = value.toLowerCase()
if(name.indexOf(lower_value) != -1 || tag.indexOf(lower_value) != -1 || el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")!=-1){
el.classList.remove('hide')
}
else{
el.classList.add('hide')
}
})
}
})
]),list,
$el('img',{id:'show_image_id',
style:{display:'none',position:'absolute'},
src:``,
onerror:()=>{
this.src = empty_img
}
})
]));
Object.defineProperty(this.widgets[styles_id],'value',{
set:(value)=>{
styles_values = value
if(styles_values){
getStylesList(styles_values).then(_=>{
selector.element.children[1].innerHTML=''
if(styles_list_cache[styles_values]){
let tags = styles_list_cache[styles_values]
// 重新排序
if(selector.value) tags = tags.sort((a,b)=> selector.value.includes(b.name) - selector.value.includes(a.name))
this.properties["values"] = []
let list = getTagList(tags, value, language);
selector.element.children[1].append(...list)
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
if (this.properties["values"].includes(el.dataset.tag)) {
el.classList.add("easyuse-prompt-styles-tag-selected");
}
if(this.size?.[0]<150 || this.size?.[1]<150) this.setSize([425, 500]);
})
}
})
}
},
get: () => {
return styles_values
}
})
let style_select_values = ''
Object.defineProperty(selector, "value", {
set: (value) => {
setTimeout(_=>{
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
let arr = value.split(',')
if (arr.includes(el.dataset.tag)) {
el.classList.add("easyuse-prompt-styles-tag-selected");
el.children[0].checked = true
}
})
},300)
},
get: () => {
selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
if(el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")>=0){
if(!this.properties["values"].includes(el.dataset.tag)){
this.properties["values"].push(el.dataset.tag);
}
}else{
if(this.properties["values"].includes(el.dataset.tag)){
this.properties["values"]= this.properties["values"].filter(v=>v!=el.dataset.tag);
}
}
});
style_select_values = this.properties["values"].join(',');
return style_select_values;
}
});
let old_values = ''
let style_lists_dom = selector.element.children[1]
style_lists_dom.addEventListener('mouseenter', function (e) {
let value = ''
style_lists_dom.querySelectorAll(".easyuse-prompt-styles-tag-selected").forEach(el=> value+=el.dataset.tag)
old_values = value
})
style_lists_dom.addEventListener('mouseleave', function (e) {
let value = ''
style_lists_dom.querySelectorAll(".easyuse-prompt-styles-tag-selected").forEach(el=> value+=el.dataset.tag)
let new_values = value
if(old_values != new_values){
// console.log("选项发生了变化")
// 获取搜索值
const search_value = document.getElementsByClassName('search')[0]['value']
// 重新排序
const tags = styles_list_cache[styles_values].sort((a,b)=> new_values.includes(b.name) - new_values.includes(a.name))
style_lists_dom.innerHTML = ''
let list = getTagList(tags, styles_values, language);
style_lists_dom.append(...list)
style_lists_dom.querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => {
if (new_values.includes(el.dataset.tag)) {
el.classList.add("easyuse-prompt-styles-tag-selected");
el.children[0].checked = true;
}
if(search_value){
if(el.dataset.name.indexOf(search_value) != -1 || el.dataset.tag.indexOf(search_value) != -1 || el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")!=-1){
el.classList.remove('hide')
}
else{
el.classList.add('hide')
}
}
})
}
})
// 初始化
setTimeout(_=>{
if(!styles_values) {
styles_values = 'fooocus_styles'
getStylesList(styles_values).then(_=>{
selector.element.children[1].innerHTML=''
if(styles_list_cache[styles_values]){
let tags = styles_list_cache[styles_values]
// 重新排序
if(selector.value) tags = tags.sort((a,b)=> selector.value.includes(b.name) - selector.value.includes(a.name))
let list = getTagList(tags, styles_values, language);
selector.element.children[1].append(...list)
}
})
}
if(this.size?.[0]<150 || this.size?.[1]<150) this.setSize([425, 500]);
//
},100)
return onNodeCreated;
}
}
}
})

View File

@@ -0,0 +1,173 @@
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { $el } from "../../../../scripts/ui.js";
import { $t } from "../common/i18n.js";
import { sleep } from "../common/utils.js";
const calculatePercent = (value, min, max) => ((value-min)/(max-min)*100)
const getLayerDefaultValue = (index) => {
switch (index){
case 3:
return 2.5
case 6:
return 1
default:
return 0
}
}
const addLayer = (_this, layer_total, arrays, sliders, i) => {
let scroll = $el('div.easyuse-slider-item-scroll')
let value = $el('div.easyuse-slider-item-input', {textContent: arrays[i]['value']})
let label = $el('div.easyuse-slider-item-label', {textContent: 'L'+i})
let girdTotal = (arrays[i]['max'] - arrays[i]['min']) / arrays[i]['step']
let area = $el('div.easyuse-slider-item-area', {style:{ height: calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max']) + '%'}})
let bar = $el('div.easyuse-slider-item-bar', {
style:{ top: (100-calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max'])) + '%'},
onmousedown: (e) => {
let event = e || window.event;
var y = event.clientY - bar.offsetTop;
document.onmousemove = (e) => {
let event = e || window.event;
let top = event.clientY - y;
if(top < 0){
top = 0;
}
else if(top > scroll.offsetHeight - bar.offsetHeight){
top = scroll.offsetHeight - bar.offsetHeight;
}
// top到最近的girdHeight值
let girlHeight = (scroll.offsetHeight - bar.offsetHeight)/ girdTotal
top = Math.round(top / girlHeight) * girlHeight;
bar.style.top = Math.floor(top/(scroll.offsetHeight - bar.offsetHeight)* 100) + '%';
area.style.height = Math.floor((scroll.offsetHeight - bar.offsetHeight - top)/(scroll.offsetHeight - bar.offsetHeight)* 100) + '%';
value.innerText = parseFloat(parseFloat(arrays[i]['max'] - (arrays[i]['max']-arrays[i]['min']) * (top/(scroll.offsetHeight - bar.offsetHeight))).toFixed(2))
arrays[i]['value'] = value.innerText
_this.properties['values'][i] = i+':'+value.innerText
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
}
},
ondblclick:_=>{
bar.style.top = (100-calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max'])) + '%'
area.style.height = calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max']) + '%'
value.innerText = arrays[i]['default']
arrays[i]['value'] = arrays[i]['default']
_this.properties['values'][i] = i+':'+value.innerText
}
})
document.onmouseup = _=> document.onmousemove = null;
scroll.replaceChildren(bar,area)
let item_div = $el('div.easyuse-slider-item',[
value,
scroll,
label
])
if(i == 3 ) layer_total == 12 ? item_div.classList.add('negative') : item_div.classList.remove('negative')
else if(i == 6) layer_total == 12 ? item_div.classList.add('positive') : item_div.classList.remove('positive')
sliders.push(item_div)
return item_div
}
const setSliderValue = (_this, type, refresh=false, values_div, sliders_value) => {
let layer_total = type == 'sdxl' ? 12 : 16
let sliders = []
let arrays = Array.from({length: layer_total}, (v, i) => ({default: layer_total == 12 ? getLayerDefaultValue(i) : 0, min: -1, max: 3, step: 0.05, value:layer_total == 12 ? getLayerDefaultValue(i) : 0}))
_this.setProperty("values", Array.from({length: layer_total}, (v, i) => i+':'+arrays[i]['value']))
for (let i = 0; i < layer_total; i++) {
addLayer(_this, layer_total, arrays, sliders, i)
}
if(refresh) values_div.replaceChildren(...sliders)
else{
values_div = $el('div.easyuse-slider', sliders)
sliders_value = _this.addDOMWidget('values',"btn",values_div)
}
Object.defineProperty(sliders_value, 'value', {
set: function() {},
get: function() {
return _this.properties.values.join(',');
}
});
return {sliders, arrays, values_div, sliders_value}
}
app.registerExtension({
name: 'comfy.easyUse.sliderControl',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData.name == 'easy sliderControl'){
// 创建时
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
onNodeCreated && onNodeCreated.call(this);
const mode = this.widgets[0];
const model_type = this.widgets[1];
let layer_total = model_type.value == 'sdxl' ? 12 : 16
let _this = this
let values_div = null
let sliders_value = null
mode.callback = async()=>{
switch (mode.value) {
case 'ipadapter layer weights':
nodeData.output_name = ['layer_weights']
_this.outputs[0]['name'] = 'layer_weights'
_this.outputs[0]['label'] = 'layer_weights'
break
}
}
model_type.callback = async()=>{
if(values_div) {
let r2 = setSliderValue(_this, model_type.value, true, values_div, sliders_value)
values_div = r2.values_div
sliders_value = r2.sliders_value
}
_this.setSize(model_type.value == 'sdxl' ? [375,320] : [455,320])
}
let r1 = setSliderValue(_this, model_type.value, false, values_div, sliders_value)
let sliders = r1.sliders
let arrays = r1.arrays
values_div = r1.values_div
sliders_value = r1.sliders_value
setTimeout(_=>{
let values_widgets_index = this.widgets.findIndex((w) => w.name == 'values');
if(values_widgets_index != -1){
let old_values_widget = this.widgets[values_widgets_index];
let old_value = old_values_widget.value.split(',')
let layer_total = _this.widgets[1].value == 'sdxl' ? 12 : 16
for (let i = 0; i < layer_total; i++) {
let value = parseFloat(parseFloat(old_value[i].split(':')[1]).toFixed(2))
let item_div = sliders[i] || null
// 存在层即修改
if(arrays[i]){
arrays[i]['value'] = value
_this.properties['values'][i] = old_value[i]
}else{
arrays.push({default: layer_total == 12 ? getLayerDefaultValue(i) : 0, min: -1, max: 3, step: 0.05, value:layer_total == 12 ? getLayerDefaultValue(i) : 0})
_this.properties['values'].push(i+':'+arrays[i]['value'])
// 添加缺失层
item_div = addLayer(_this, layer_total, arrays, sliders, i)
values_div.appendChild(item_div)
}
// todo: 修改bar位置等
let input = item_div.getElementsByClassName('easyuse-slider-item-input')[0]
let bar = item_div.getElementsByClassName('easyuse-slider-item-bar')[0]
let area = item_div.getElementsByClassName('easyuse-slider-item-area')[0]
if(i == 3 ) layer_total == 12 ? item_div.classList.add('negative') : item_div.classList.remove('negative')
else if(i == 6) layer_total == 12 ? item_div.classList.add('positive') : item_div.classList.remove('positive')
input.textContent = value
bar.style.top = (100-calculatePercent(value,arrays[i]['min'],arrays[i]['max'])) + '%'
area.style.height = calculatePercent(value,arrays[i]['min'],arrays[i]['max']) + '%'
}
}
_this.setSize(model_type.value == 'sdxl' ? [375,320] : [455,320])
},1)
return onNodeCreated;
}
}
}
})

View File

@@ -0,0 +1,554 @@
import {app} from "../../../../scripts/app.js";
import {api} from "../../../../scripts/api.js";
import {$el} from "../../../../scripts/ui.js";
const propmts = ["easy wildcards", "easy positive", "easy negative", "easy stylesSelector", "easy promptConcat", "easy promptReplace"]
const loaders = ["easy a1111Loader", "easy comfyLoader", "easy fullLoader", "easy svdLoader", "easy cascadeLoader", "easy sv3dLoader"]
const preSamplingNodes = ["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingCustom", "easy preSamplingDynamicCFG","easy preSamplingSdTurbo", "easy preSamplingLayerDiffusion"]
const kSampler = ["easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo"]
const controlNetNodes = ["easy controlnetLoader", "easy controlnetLoaderADV"]
const instantIDNodes = ["easy instantIDApply", "easy instantIDApplyADV"]
const ipadapterNodes = ["easy ipadapterApply", "easy ipadapterApplyADV" ,"easy ipadapterApplyFaceIDKolors", "easy ipadapterStyleComposition"]
const pipeNodes = ['easy pipeIn','easy pipeOut', 'easy pipeEdit']
const xyNodes = ['easy XYPlot', 'easy XYPlotAdvanced']
const extraNodes = ['easy setNode']
const modelNormalNodes = [...['RescaleCFG','LoraLoaderModelOnly','LoraLoader','FreeU','FreeU_v2'],...ipadapterNodes,...extraNodes]
const suggestions = {
// prompt
"easy seed":{
"from":{
"INT": [...preSamplingNodes,...['easy fullkSampler']]
}
},
"easy positive":{
"from":{
"STRING": [...propmts]
}
},
"easy negative":{
"from":{
"STRING": [...propmts]
}
},
"easy wildcards":{
"from":{
"STRING": [...["Reroute","easy showAnything"],...propmts,]
}
},
"easy stylesSelector":{
"from":{
"STRING": [...["Reroute","easy showAnything"],...propmts,]
}
},
"easy promptConcat":{
"from":{
"STRING": [...["Reroute","easy showAnything"],...propmts,]
}
},
"easy promptReplace":{
"from":{
"STRING": [...["Reroute","easy showAnything"],...propmts,]
}
},
// sd相关
"easy fullLoader": {
"from":{
"PIPE_LINE": [...preSamplingNodes,...['easy fullkSampler'],...pipeNodes,...extraNodes],
"MODEL":modelNormalNodes
},
"to":{
"STRING": [...propmts]
}
},
"easy a1111Loader": {
"from": {
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...propmts]
}
},
"easy comfyLoader": {
"from": {
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...propmts]
}
},
"easy svdLoader":{
"from": {
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...propmts]
}
},
"easy zero123Loader":{
"from": {
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...propmts]
}
},
"easy sv3dLoader":{
"from": {
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...propmts]
}
},
"easy preSampling": {
"from": {
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
},
},
"easy preSamplingAdvanced": {
"from": {
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingDynamicCFG": {
"from": {
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingCustom": {
"from": {
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingLayerDiffusion": {
"from": {
"PIPE_LINE": [...["easy kSamplerLayerDiffusion"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingNoiseIn": {
"from": {
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
// ksampler
"easy fullkSampler": {
"from": {
"PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix'], ...preSamplingNodes, ...extraNodes]
}
},
"easy kSampler": {
"from": {
"PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix', 'easy hiresFix'], ...preSamplingNodes, ...extraNodes],
}
},
// cn
"easy controlnetLoader": {
"from": {
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
}
},
"easy controlnetLoaderADV":{
"from": {
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
}
},
// instant
"easy instantIDApply": {
"from": {
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"COMBO": [...["easy promptLine"]]
}
},
"easy instantIDApplyADV":{
"from": {
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterApply":{
"to":{
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterApplyADV":{
"to":{
"STRING": [...["easy sliderControl"], ...propmts],
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterStyleComposition":{
"to":{
"COMBO": [...["easy promptLine"]]
}
},
// fix
"easy preDetailerFix":{
"from": {
"PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes]
},
"to":{
"PIPE_LINE": [...["easy ultralyticsDetectorPipe", "easy samLoaderPipe", "easy kSampler", "easy fullkSampler"]]
}
},
"easy preMaskDetailerFix":{
"from": {
"PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes]
}
},
"easy samLoaderPipe": {
"from":{
"PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes]
}
},
"easy ultralyticsDetectorPipe": {
"from":{
"PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes]
}
},
// cascade相关
"easy cascadeLoader":{
"from": {
"PIPE_LINE": [ ...["easy fullCascadeKSampler", 'easy preSamplingCascade'], ...controlNetNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes.filter(cate => !ipadapterNodes.includes(cate))
}
},
"easy fullCascadeKSampler":{
"from": {
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
}
},
"easy preSamplingCascade":{
"from": {
"PIPE_LINE": [ ...["easy cascadeKSampler",], ...pipeNodes, ...extraNodes]
}
},
"easy cascadeKSampler": {
"from": {
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
}
},
}
class NullGraphError extends Error {
constructor(message="Attempted to access LGraph reference that was null or undefined.", cause) {
super(message, {cause})
this.name = "NullGraphError"
}
}
app.registerExtension({
name: "comfy.easyuse.suggestions",
async setup() {
const createDefaultNodeForSlot = LGraphCanvas.prototype.createDefaultNodeForSlot;
LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection
const opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,position: [] // pass the event coords
,nodeType: null // choose a nodetype to add, AUTO to set at first good
,posAdd:[0,0] // adjust x,y
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h
}
, optPass || {}
);
const { afterRerouteId } = opts
const that = this;
const isFrom = opts.nodeFrom && opts.slotFrom!==null;
const isTo = !isFrom && opts.nodeTo && opts.slotTo!==null;
const node = isFrom ? opts.nodeFrom : opts.nodeTo
// Not an Easy Use node, skip showConnectionMenu hijack
if(!node || !Object.keys(suggestions).includes(node.type)){
return createDefaultNodeForSlot.call(this, optPass)
}
if (!isFrom && !isTo){
console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo);
return false;
}
if (!opts.nodeType){
console.warn("No type to createDefaultNodeForSlot");
return false;
}
const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
const nodeType = nodeX.type
let slotX = isFrom ? opts.slotFrom : opts.slotTo;
let iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
case "undefined":
default:
// bad ?
//iSlotConn = 0;
console.warn("Cant get slot information "+slotX);
return false;
}
if (slotX===false || iSlotConn===false){
console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn);
}
// check for defaults nodes for this slottype
var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type;
var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in;
if(slotTypesDefault && slotTypesDefault[fromSlotType]){
if (slotX.link !== null) {
// is connected
}else{
// is not not connected
}
let nodeNewType = false;
const fromOrTo = isFrom ? 'from' : 'to'
if(suggestions[nodeType] && suggestions[nodeType][fromOrTo] && suggestions[nodeType][fromOrTo][fromSlotType]?.length>0){
for(var typeX in suggestions[nodeType][fromOrTo][fromSlotType]){
if (opts.nodeType == suggestions[nodeType][fromOrTo][fromSlotType][typeX] || opts.nodeType == "AUTO") {
nodeNewType = suggestions[nodeType][fromOrTo][fromSlotType][typeX];
break
}
}
}
else if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){
for(var typeX in slotTypesDefault[fromSlotType]){
if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){
nodeNewType = slotTypesDefault[fromSlotType][typeX];
break;
}
}
}else{
if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType];
}
if (nodeNewType) {
var nodeNewOpts = false;
if (typeof nodeNewType == "object" && nodeNewType.node){
nodeNewOpts = nodeNewType;
nodeNewType = nodeNewType.node;
}
//that.graph.beforeChange();
var newNode = LiteGraph.createNode(nodeNewType);
if(newNode){
// if is object pass options
if (nodeNewOpts){
if (nodeNewOpts.properties) {
for (var i in nodeNewOpts.properties) {
newNode.addProperty( i, nodeNewOpts.properties[i] );
}
}
if (nodeNewOpts.inputs) {
newNode.inputs = [];
for (var i in nodeNewOpts.inputs) {
newNode.addOutput(
nodeNewOpts.inputs[i][0],
nodeNewOpts.inputs[i][1]
);
}
}
if (nodeNewOpts.outputs) {
newNode.outputs = [];
for (var i in nodeNewOpts.outputs) {
newNode.addOutput(
nodeNewOpts.outputs[i][0],
nodeNewOpts.outputs[i][1]
);
}
}
if (nodeNewOpts.title) {
newNode.title = nodeNewOpts.title;
}
if (nodeNewOpts.json) {
newNode.configure(nodeNewOpts.json);
}
}
// add the node
that.graph.add(newNode);
newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0)
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/
// connect the two!
if (isFrom){
opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType );
}else{
opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType );
}
return true;
}else{
console.log("failed creating "+nodeNewType);
}
}
}
return false;
}
let showConnectionMenu = LGraphCanvas.prototype.showConnectionMenu
LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection
const opts = Object.assign({
nodeFrom: null, // input
slotFrom: null, // input
nodeTo: null, // output
slotTo: null, // output
e: undefined,
allow_searchbox: this.allow_searchbox,
showSearchBox: this.showSearchBox,
}
,optPass || {}
);
const that = this;
const { graph } = this
const { afterRerouteId } = opts
const isFrom = opts.nodeFrom && opts.slotFrom;
const isTo = !isFrom && opts.nodeTo && opts.slotTo;
const node = isFrom ? opts.nodeFrom : opts.nodeTo
// Not an Easy Use node, skip showConnectionMenu hijack
if(!node || !Object.keys(suggestions).includes(node.type)){
return showConnectionMenu.call(this, optPass)
}
if (!isFrom && !isTo){
console.warn("No data passed to showConnectionMenu");
return false;
}
const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
if (!nodeX) throw new TypeError("nodeX was null when creating default node for slot.")
let slotX = isFrom ? opts.slotFrom : opts.slotTo;
let iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
default:
// bad ?
//iSlotConn = 0;
console.warn("Cant get slot information "+slotX);
return false;
}
const options = ["Add Node", "Add Reroute", null]
if (opts.allow_searchbox){
options.push("Search");
options.push(null);
}
// get defaults nodes for this slottype
var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type;
var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in;
var nodeType = nodeX.type
if(slotTypesDefault && slotTypesDefault[fromSlotType]){
const fromOrTo = isFrom ? 'from' : 'to'
if(suggestions[nodeType] && suggestions[nodeType][fromOrTo] && suggestions[nodeType][fromOrTo][fromSlotType]?.length>0){
for(var typeX in suggestions[nodeType][fromOrTo][fromSlotType]){
options.push(suggestions[nodeType][fromOrTo][fromSlotType][typeX]);
}
}
else if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){
for(var typeX in slotTypesDefault[fromSlotType]){
options.push(slotTypesDefault[fromSlotType][typeX]);
}
}else{
options.push(slotTypesDefault[fromSlotType]);
}
}
// build menu
var menu = new LiteGraph.ContextMenu(options, {
event: opts.e,
extra: slotX,
title:
(slotX && slotX.name != ""
? slotX.name + (fromSlotType ? " | " : "")
: "") + (slotX && fromSlotType ? fromSlotType : ""),
callback: inner_clicked,
});
// callback
function inner_clicked(v,options,e) {
//console.log("Process showConnectionMenu selection");
switch (v) {
case "Add Node":
LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) {
if (!node) return
if (isFrom) {
opts.nodeFrom?.connectByType(iSlotConn, node, fromSlotType, { afterRerouteId })
} else {
opts.nodeTo?.connectByTypeOutput(iSlotConn, node, fromSlotType, { afterRerouteId })
}
})
break;
case "Add Reroute":
const node = isFrom ? opts.nodeFrom : opts.nodeTo
const slot = options.extra
if (!graph) throw new NullGraphError()
if (!node) throw new TypeError("Cannot add reroute: node was null")
if (!slot) throw new TypeError("Cannot add reroute: slot was null")
if (!opts.e) throw new TypeError("Cannot add reroute: CanvasPointerEvent was null")
const reroute = node.connectFloatingReroute([opts.e.canvasX, opts.e.canvasY], slot, afterRerouteId)
if (!reroute) throw new Error("Failed to create reroute")
that.dirty_canvas = true
that.dirty_bgcanvas = true
break
case "Search":
if(isFrom){
opts.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType});
}else{
opts.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType});
}
break;
default:
const customProps = {
position: [opts.e?.canvasX ?? 0, opts.e?.canvasY ?? 0],
nodeType: v,
afterRerouteId,
}
// check for defaults nodes for this slottype
that.createDefaultNodeForSlot(Object.assign(opts, customProps))
break;
}
}
return false;
};
}
})

View File

@@ -0,0 +1,400 @@
import { app } from "../../../../scripts/app.js";
import { ComfyWidgets } from "../../../../scripts/widgets.js";
const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 };
const WIDGET_GAP = -4;
function hideInfoWidget(e, node, widget) {
let dropdownShouldBeRemoved = false;
let selectionIndex = -1;
if (e) {
e.preventDefault();
e.stopPropagation();
displayDropdown(widget);
} else {
hideWidget(widget, node);
}
function createDropdownElement() {
const dropdown = document.createElement('ul');
dropdown.id = 'hideinfo-dropdown';
dropdown.setAttribute('role', 'listbox');
dropdown.classList.add('hideInfo-dropdown');
return dropdown;
}
function createDropdownItem(textContent, action) {
const listItem = document.createElement('li');
listItem.id = `hideInfo-item-${textContent.replace(/ /g, '')}`;
listItem.classList.add('hideInfo-item');
listItem.setAttribute('role', 'option');
listItem.textContent = textContent;
listItem.addEventListener('mousedown', (event) => {
event.preventDefault();
action(widget, node); // perform the action when dropdown item is clicked
removeDropdown();
dropdownShouldBeRemoved = false;
});
listItem.dataset.action = textContent.replace(/ /g, ''); // store the action in a data attribute
return listItem;
}
function displayDropdown(widget) {
removeDropdown();
const dropdown = createDropdownElement();
const listItemHide = createDropdownItem('Hide info Widget', hideWidget);
const listItemHideAll = createDropdownItem('Hide for all of this node-type', hideWidgetForNodetype);
dropdown.appendChild(listItemHide);
dropdown.appendChild(listItemHideAll);
const inputRect = widget.inputEl.getBoundingClientRect();
dropdown.style.top = `${inputRect.top + inputRect.height}px`;
dropdown.style.left = `${inputRect.left}px`;
dropdown.style.width = `${inputRect.width}px`;
document.body.appendChild(dropdown);
dropdownShouldBeRemoved = true;
widget.inputEl.removeEventListener('keydown', handleKeyDown);
widget.inputEl.addEventListener('keydown', handleKeyDown);
document.addEventListener('click', handleDocumentClick);
}
function removeDropdown() {
const dropdown = document.getElementById('hideinfo-dropdown');
if (dropdown) {
dropdown.remove();
widget.inputEl.removeEventListener('keydown', handleKeyDown);
}
document.removeEventListener('click', handleDocumentClick);
}
function handleKeyDown(event) {
const dropdownItems = document.querySelectorAll('.hideInfo-item');
if (event.keyCode === KEY_CODES.ENTER && dropdownShouldBeRemoved) {
event.preventDefault();
if (selectionIndex !== -1) {
const selectedAction = dropdownItems[selectionIndex].dataset.action;
if (selectedAction === 'HideinfoWidget') {
hideWidget(widget, node);
} else if (selectedAction === 'Hideforall') {
hideWidgetForNodetype(widget, node);
}
removeDropdown();
dropdownShouldBeRemoved = false;
}
} else if (event.keyCode === KEY_CODES.ARROW_DOWN && dropdownShouldBeRemoved) {
event.preventDefault();
if (selectionIndex !== -1) {
dropdownItems[selectionIndex].classList.remove('selected');
}
selectionIndex = (selectionIndex + 1) % dropdownItems.length;
dropdownItems[selectionIndex].classList.add('selected');
} else if (event.keyCode === KEY_CODES.ARROW_UP && dropdownShouldBeRemoved) {
event.preventDefault();
if (selectionIndex !== -1) {
dropdownItems[selectionIndex].classList.remove('selected');
}
selectionIndex = (selectionIndex - 1 + dropdownItems.length) % dropdownItems.length;
dropdownItems[selectionIndex].classList.add('selected');
} else if (event.keyCode === KEY_CODES.ESC && dropdownShouldBeRemoved) {
event.preventDefault();
removeDropdown();
}
}
function hideWidget(widget, node) {
node.properties['infoWidgetHidden'] = true;
widget.type = "esayHidden";
widget.computeSize = () => [0, WIDGET_GAP];
node.setSize([node.size[0], node.size[1]]);
}
function hideWidgetForNodetype(widget, node) {
hideWidget(widget, node)
const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
if (!hiddenNodeTypes.includes(node.constructor.type)) {
hiddenNodeTypes.push(node.constructor.type);
}
localStorage.setItem('hiddenWidgetNodeTypes', JSON.stringify(hiddenNodeTypes));
}
function handleDocumentClick(event) {
const dropdown = document.getElementById('hideinfo-dropdown');
// If the click was outside the dropdown and the dropdown should be removed, remove it
if (dropdown && !dropdown.contains(event.target) && dropdownShouldBeRemoved) {
removeDropdown();
dropdownShouldBeRemoved = false;
}
}
}
var styleElement = document.createElement("style");
const cssCode = `
.easy-info_widget {
background-color: var(--comfy-input-bg);
color: var(--input-text);
overflow: hidden;
padding: 2px;
resize: none;
border: none;
box-sizing: border-box;
font-size: 10px;
border-radius: 7px;
text-align: center;
text-wrap: balance;
}
.hideInfo-dropdown {
position: absolute;
box-sizing: border-box;
background-color: #121212;
border-radius: 7px;
box-shadow: 0 2px 4px rgba(255, 255, 255, .25);
padding: 0;
margin: 0;
list-style: none;
z-index: 1000;
overflow: auto;
max-height: 200px;
}
.hideInfo-dropdown li {
padding: 4px 10px;
cursor: pointer;
font-family: system-ui;
font-size: 0.7rem;
}
.hideInfo-dropdown li:hover,
.hideInfo-dropdown li.selected {
background-color: #e5e5e5;
border-radius: 7px;
}
`
styleElement.innerHTML = cssCode
document.head.appendChild(styleElement);
const InfoSymbol = Symbol();
const InfoResizeSymbol = Symbol();
// WIDGET FUNCTIONS
function addInfoWidget(node, name, opts, app) {
const INFO_W_SIZE = 50;
node.addProperty('infoWidgetHidden', false)
function computeSize(size) {
if (node.widgets[0].last_y == null) return;
let y = node.widgets[0].last_y;
// Compute the height of all non easyInfo widgets
let widgetHeight = 0;
const infoWidges = [];
for (let i = 0; i < node.widgets.length; i++) {
const w = node.widgets[i];
if (w.type === "easyInfo") {
infoWidges.push(w);
} else {
if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
}
let infoWidgetSpace = infoWidges.length * INFO_W_SIZE; // Height for all info widgets
// Check if there's enough space for all widgets
if (size[1] < y + widgetHeight + infoWidgetSpace) {
// There isn't enough space for all the widgets, increase the size of the node
node.size[1] = y + widgetHeight + infoWidgetSpace;
node.graph.setDirtyCanvas(true);
}
// Position each of the widgets
for (const w of node.widgets) {
w.y = y;
if (w.type === "easyInfo") {
y += INFO_W_SIZE;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
}
const widget = {
type: "easyInfo",
name,
get value() {
return this.inputEl.value;
},
set value(x) {
this.inputEl.value = x;
},
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
if (!this.parent.inputHeight) {
// If we are initially offscreen when created we wont have received a resize event
// Calculate it here instead
computeSize(node.size);
}
const visible = app.canvas.ds.scale > 0.5 && this.type === "easyInfo";
const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
Object.assign(this.inputEl.style, {
transformOrigin: "0 0",
transform: transform,
left: "0px",
top: "0px",
width: `${widgetWidth - (margin * 2)}px`,
height: `${this.parent.inputHeight - (margin * 2)}px`,
position: "absolute",
background: (!node.color)?'':node.color,
color: (!node.color)?'':'white',
zIndex: app.graph._nodes.indexOf(node),
});
this.inputEl.hidden = !visible;
},
};
widget.inputEl = document.createElement("textarea");
widget.inputEl.className = "easy-info_widget";
widget.inputEl.value = opts.defaultVal;
widget.inputEl.placeholder = opts.placeholder || "";
widget.inputEl.readOnly = true;
widget.parent = node;
document.body.appendChild(widget.inputEl);
node.addCustomWidget(widget);
app.canvas.onDrawBackground = function () {
// Draw node isnt fired once the node is off the screen
// if it goes off screen quickly, the input may not be removed
// this shifts it off screen so it can be moved back if the node is visible.
for (let n in app.graph._nodes) {
n = app.graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "inputEl")) {
wid.inputEl.style.left = -8000 + "px";
wid.inputEl.style.position = "absolute";
}
}
}
};
node.onRemoved = function () {
// When removing this node we need to remove the input from the DOM
for (let y in this.widgets) {
if (this.widgets[y].inputEl) {
this.widgets[y].inputEl.remove();
}
}
};
widget.onRemove = () => {
widget.inputEl?.remove();
// Restore original size handler if we are the last
if (!--node[InfoSymbol]) {
node.onResize = node[InfoResizeSymbol];
delete node[InfoSymbol];
delete node[InfoResizeSymbol];
}
};
if (node[InfoSymbol]) {
node[InfoSymbol]++;
} else {
node[InfoSymbol] = 1;
const onResize = (node[InfoResizeSymbol] = node.onResize);
node.onResize = function (size) {
computeSize(size);
// Call original resizer handler
if (onResize) {
console.log(this, arguments)
onResize.apply(this, arguments);
}
};
}
return { widget };
}
// WIDGETS
const easyCustomWidgets = {
INFO(node, inputName, inputData, app) {
const defaultVal = inputData[1].default || "";
return addInfoWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
},
}
app.registerExtension({
name: "comfy.easy.widgets",
getCustomWidgets(app) {
return easyCustomWidgets;
},
nodeCreated(node) {
if (node.widgets) {
// Locate info widgets
const widgets = node.widgets.filter((n) => (n.type === "easyInfo"));
for (const widget of widgets) {
widget.inputEl.addEventListener('contextmenu', function(e) {
hideInfoWidget(e, node, widget);
});
widget.inputEl.addEventListener('click', function(e) {
hideInfoWidget(e, node, widget);
});
}
}
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
if (this.properties['infoWidgetHidden']) {
for (let i in this.widgets) {
if (this.widgets[i].type == "easyInfo") {
hideInfoWidget(null, this, this.widgets[i]);
}
}
}
return r;
};
const origOnAdded = nodeType.prototype.onAdded;
nodeType.prototype.onAdded = function () {
const r = origOnAdded ? origOnAdded.apply(this, arguments) : undefined;
if (hiddenNodeTypes.includes(this.type)) {
for (let i in this.widgets) {
if (this.widgets[i].type == "easyInfo") {
this.properties['infoWidgetHidden'] = true;
}
}
}
return r;
}
}
});

View File

@@ -0,0 +1,212 @@
import { app } from "../../../../scripts/app.js";
import {removeDropdown, createDropdown} from "../common/dropdown.js";
function generateNumList(dictionary) {
const minimum = dictionary["min"] || 0;
const maximum = dictionary["max"] || 0;
const step = dictionary["step"] || 1;
if (step === 0) {
return [];
}
const result = [];
let currentValue = minimum;
while (currentValue <= maximum) {
if (Number.isInteger(step)) {
result.push(Math.round(currentValue) + '; ');
} else {
let formattedValue = currentValue.toFixed(3);
if(formattedValue == -0.000){
formattedValue = '0.000';
}
if (!/\.\d{3}$/.test(formattedValue)) {
formattedValue += "0";
}
result.push(formattedValue + "; ");
}
currentValue += step;
}
if (maximum >= 0 && minimum >= 0) {
//low to high
return result;
}
else {
//high to low
return result.reverse();
}
}
let plotDict = {};
let currentOptionsDict = {};
function getCurrentOptionLists(node, widget) {
const nodeId = String(node.id);
const widgetName = widget.name;
const widgetValue = widget.value.replace(/^(loader|preSampling):\s/, '');
if (!currentOptionsDict[widgetName]) {
currentOptionsDict = {...currentOptionsDict, [widgetName]: plotDict[widgetValue]};
} else if (currentOptionsDict[widgetName] != plotDict[widgetValue]) {
currentOptionsDict[widgetName] = plotDict[widgetValue];
}
}
function addGetSetters(node) {
if (node.widgets)
for (const w of node.widgets) {
if (w.name === "x_axis" ||
w.name === "y_axis") {
let widgetValue = w.value;
// Define getters and setters for widget values
Object.defineProperty(w, 'value', {
get() {
return widgetValue;
},
set(newVal) {
if (newVal !== widgetValue) {
widgetValue = newVal;
getCurrentOptionLists(node, w);
}
}
});
}
}
}
function dropdownCreator(node) {
if (node.widgets) {
const widgets = node.widgets.filter(
(n) => (n.type === "customtext" && n.dynamicPrompts !== false) || n.dynamicPrompts
);
for (const w of widgets) {
function replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList) {
if (selectedOption) {
inputSegments[cursorSegmentIndex] = selectedOption;
}
return inputSegments.map(segment => verifySegment(segment, optionsList))
.filter(item => item !== '')
.join('');
}
function verifySegment(segment, optionsList) {
segment = cleanSegment(segment);
if (isInOptionsList(segment, optionsList)) {
return segment + '; ';
}
let matchedOptions = findMatchedOptions(segment, optionsList);
if (matchedOptions.length === 1 || matchedOptions.length === 2) {
return matchedOptions[0];
}
if (isInOptionsList(formatNumberSegment(segment), optionsList)) {
return formatNumberSegment(segment) + '; ';
}
return '';
}
function cleanSegment(segment) {
return segment.replace(/(\n|;| )/g, '');
}
function isInOptionsList(segment, optionsList) {
return optionsList.includes(segment + '; ');
}
function findMatchedOptions(segment, optionsList) {
return optionsList.filter(option => option.toLowerCase().includes(segment.toLowerCase()));
}
function formatNumberSegment(segment) {
if (Number(segment)) {
return Number(segment).toFixed(3);
}
if (['0', '0.', '0.0', '0.00', '00'].includes(segment)) {
return '0.000';
}
return segment;
}
const onInput = function () {
const axisWidgetName = w.name[0] + '_axis';
let optionsList = currentOptionsDict?.[axisWidgetName] || [];
if (optionsList.length === 0) {return}
const inputText = w.inputEl.value;
const cursorPosition = w.inputEl.selectionStart;
let inputSegments = inputText.split('; ');
const cursorSegmentIndex = inputText.substring(0, cursorPosition).split('; ').length - 1;
const currentSegment = inputSegments[cursorSegmentIndex];
const currentSegmentLower = currentSegment.replace(/\n/g, '').toLowerCase();
const filteredOptionsList = optionsList.filter(option => option.toLowerCase().includes(currentSegmentLower)).map(option => option.replace(/; /g, ''));
if (filteredOptionsList.length > 0) {
createDropdown(w.inputEl, filteredOptionsList, (selectedOption) => {
const verifiedText = replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList);
w.inputEl.value = verifiedText;
});
}
else {
removeDropdown();
const verifiedText = replaceOptionSegments(null, inputSegments, cursorSegmentIndex, optionsList);
w.inputEl.value = verifiedText;
}
};
w.inputEl.removeEventListener('input', onInput);
w.inputEl.addEventListener('input', onInput);
w.inputEl.removeEventListener('mouseup', onInput);
w.inputEl.addEventListener('mouseup', onInput);
}
}
}
app.registerExtension({
name: "comfy.easy.xyPlot",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "easy XYPlot") {
plotDict = nodeData.input.hidden.plot_dict[0];
for (const key in plotDict) {
const value = plotDict[key];
if (Array.isArray(value)) {
let updatedValues = [];
for (const v of value) {
updatedValues.push(v + '; ');
}
plotDict[key] = updatedValues;
} else if (typeof(value) === 'object') {
if(key == 'seed'){
plotDict[key] = value + '; ';
}
else {
plotDict[key] = generateNumList(value);
}
} else {
plotDict[key] = value + '; ';
}
}
plotDict["None"] = [];
plotDict["---------------------"] = [];
}
},
nodeCreated(node) {
if (node.comfyClass === "easy XYPlot") {
addGetSetters(node);
dropdownCreator(node);
}
}
});