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,93 @@
.easyuse-account{
}
.easyuse-account-user{
font-size: 10px;
color:var(--descrip-text);
text-align: center;
}
.easyuse-account-user-info{
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom:10px;
cursor: pointer;
}
.easyuse-account-user-info .user{
display: flex;
align-items: center;
}
.easyuse-account-user-info .edit{
padding:5px 10px;
background: var(--comfy-menu-bg);
border-radius:4px;
}
.easyuse-account-user-info:hover{
filter:brightness(110%);
}
.easyuse-account-user-info h5{
margin:0;
font-size: 10px;
text-align: left;
}
.easyuse-account-user-info h6{
margin:0;
font-size: 8px;
text-align: left;
font-weight: 300;
}
.easyuse-account-user-info .remark{
margin-top: 4px;
}
.easyuse-account-user-info .avatar{
width: 36px;
height: 36px;
background: var(--comfy-input-bg);
border-radius: 50%;
margin-right: 5px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
overflow: hidden;
}
.easyuse-account-user-info .avatar img{
width: 100%;
height: 100%;
}
.easyuse-account-dialog{
width: 600px;
}
.easyuse-account-dialog-main a, .easyuse-account-dialog-main a:visited{
font-weight: 400;
color: var(--theme-color-light);
}
.easyuse-account-dialog-item{
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.easyuse-account-dialog-item input{
padding:5px;
margin-right:5px;
}
.easyuse-account-dialog-item input.key{
flex:1;
}
.easyuse-account-dialog-item button{
cursor: pointer;
margin-left:5px!important;
padding:5px!important;
font-size: 16px!important;
}
.easyuse-account-dialog-item button:hover{
filter:brightness(120%);
}
.easyuse-account-dialog-item button.choose {
background: var(--theme-color);
}
.easyuse-account-dialog-item button.delete{
background: var(--error-color);
}

View File

@@ -0,0 +1,35 @@
.easyuse-chooser-dialog{
max-width: 600px;
}
.easyuse-chooser-dialog-title{
font-size: 18px;
font-weight: 700;
text-align: center;
color:var(--input-text);
margin:0;
}
.easyuse-chooser-dialog-images{
margin-top:10px;
display: flex;
flex-wrap: wrap;
width: 100%;
box-sizing: border-box;
}
.easyuse-chooser-dialog-images img{
width: 50%;
height: auto;
cursor: pointer;
box-sizing: border-box;
filter:brightness(80%);
}
.easyuse-chooser-dialog-images img:hover{
filter:brightness(100%);
}
.easyuse-chooser-dialog-images img.selected{
border: 4px solid var(--success-color);
}
.easyuse-chooser-hidden{
display: none;
height:0;
}

View File

@@ -0,0 +1,20 @@
.easyuse-model{
position:relative;
}
.easyuse-model:hover img{
display: block;
opacity: 1;
}
.easyuse-model img{
position: absolute;
z-index:1;
right:-155px;
top:0;
width:150px;
height:auto;
display: none;
filter:brightness(70%);
-webkit-filter: brightness(70%);
opacity: 0;
transition:all 0.5s ease-in-out;
}

View File

@@ -0,0 +1,68 @@
.easy-dropdown, .easy-nested-dropdown {
position: relative;
box-sizing: border-box;
background-color: #171717;
box-shadow: 0 4px 4px rgba(255, 255, 255, .25);
padding: 0;
margin: 0;
list-style: none;
z-index: 1000;
overflow: visible;
max-height: fit-content;
max-width: fit-content;
}
.easy-dropdown {
position: absolute;
border-radius: 0;
}
/* Style for final items */
.easy-dropdown li.item, .easy-nested-dropdown li.item {
font-weight: normal;
min-width: max-content;
}
/* Style for folders (parent items) */
.easy-dropdown li.folder, .easy-nested-dropdown li.folder {
cursor: default;
position: relative;
border-right: 3px solid cyan;
}
.easy-dropdown li.folder::after, .easy-nested-dropdown li.folder::after {
content: ">";
position: absolute;
right: 2px;
font-weight: normal;
}
.easy-dropdown li, .easy-nested-dropdown li {
padding: 4px 10px;
cursor: pointer;
font-family: system-ui;
font-size: 0.7rem;
position: relative;
}
/* Style for nested dropdowns */
.easy-nested-dropdown {
position: absolute;
top: 0;
left: 100%;
margin: 0;
border: none;
display: none;
}
.easy-dropdown li.selected > .easy-nested-dropdown,
.easy-nested-dropdown li.selected > .easy-nested-dropdown {
display: block;
border: none;
}
.easy-dropdown li.selected,
.easy-nested-dropdown li.selected {
background-color: #e5e5e5;
border: none;
}

View File

@@ -0,0 +1,130 @@
.pysssss-workflow-popup{
min-width:220px!important;
/*right:0px!important;*/
/*left:auto!important;*/
}
body{
font-family: var(--font-family)!important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
textarea{
font-family: var(--font-family)!important;
}
.comfy-multiline-input{
background-color: transparent;
border:1px solid var(--border-color);
border-radius:8px;
padding: 8px;
font-size: 12px;
}
.comfy-modal {
border:1px solid var(--border-color);
box-shadow:none;
backdrop-filter: blur(8px) brightness(120%);
}
.comfy-menu{
border-radius:16px;
box-shadow:0 0 1px var(--descrip-text);
backdrop-filter: blur(8px) brightness(120%);
}
.comfy-menu button,.comfy-modal button {
font-size: 14px;
padding:4px 0;
margin-bottom:4px;
}
.comfy-menu button.comfy-settings-btn{
font-size: 12px;
}
.comfy-menu-btns {
margin-bottom: 4px;
}
.comfy-menu-btns button,.comfy-list-actions button{
font-size: 10px;
}
.comfy-menu > button,
.comfy-menu-btns button,
.comfy-menu .comfy-list button,
.comfy-modal button {
border-width:1px;
}
.comfy-modal-content{
width: 100%;
}
dialog{
border:1px solid var(--border-color);
background:transparent;
backdrop-filter: blur(8px) brightness(120%);
box-shadow:none;
}
.cm-title{
background-color:transparent!important;
}
.cm-notice-board{
border-radius:10px!important;
border:1px solid var(--border-color)!important;
}
.cm-menu-container{
margin-bottom:50px!important;
}
hr{
border:1px solid var(--border-color);
}
#comfy-dev-save-api-button{
justify-content: center;
}
#shareButton{
background:linear-gradient(to left,var(--theme-color),var(--theme-color-light))!important;
color:white!important;
}
#queue-button{
position:relative;
overflow:hidden;
min-height:30px;
z-index:1;
}
#queue-button:after{
clear: both;
content:attr(data-attr);
background:green;
color:#FFF;
width:var(--process-bar-width);
height:100%;
position:absolute;
top:0;
left:0;
z-index:0;
text-align:center;
display:flex;
justify-content:center;
align-items:center;
}
.litegraph .litemenu-entry.has_submenu {
border-right: 2px solid var(--theme-color);
}
::-webkit-scrollbar {
width: 0em;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background-color: transparent;
}
[data-theme="dark"] .workspace_manager .chakra-card{
background-color:var(--comfy-menu-bg)!important;
}
.workspace_manager .chakra-card{
width: 400px;
}

View File

@@ -0,0 +1,34 @@
#easyuse_groups_map{
flex-direction: column;
align-items: end;
display:flex;position: absolute;
top: 50px; left: 10px; width: 180px;
border-radius:12px;
min-height:100px;
max-height:400px;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
padding: 10px 4px;
border: 1px solid var(--border-color);
z-index: 399;
padding-top: 0;
}
#easyuse_groups_map .icon{
width: 12px;
height:12px;
}
#easyuse_groups_map .closeBtn{
float: right;
color: var(--input-text);
border-radius:30px;
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
cursor: pointer;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
}
#easyuse_groups_map .closeBtn:hover{
filter:brightness(120%);
}

View File

@@ -0,0 +1,11 @@
@import "theme.css";
@import "dropdown.css";
@import "selector.css";
@import "groupmap.css";
@import "contextmenu.css";
@import "modelinfo.css";
@import "toast.css";
@import "account.css";
@import "chooser.css";
@import "toolbar.css";
@import "sliderControl.css";

View File

@@ -0,0 +1,265 @@
.easyuse-model-info {
color: white;
max-width: 90vw;
font-family: var(--font-family);
}
.easyuse-model-content {
display: flex;
flex-direction: column;
overflow: hidden;
}
.easyuse-model-header{
margin:0 0 15px 0;
}
.easyuse-model-header-remark{
display: flex;
align-items: center;
margin-top:5px;
}
.easyuse-model-info h2 {
text-align: left;
margin:0;
}
.easyuse-model-info h5 {
text-align: left;
margin:0 15px 0 0px;
font-weight: 400;
color:var(--descrip-text);
}
.easyuse-model-info p {
margin: 5px 0;
}
.easyuse-model-info a {
color: var(--theme-color-light);
}
.easyuse-model-info a:hover {
text-decoration: underline;
}
.easyuse-model-tags-list {
display: flex;
flex-wrap: wrap;
list-style: none;
gap: 10px;
max-height: 200px;
overflow: auto;
margin: 10px 0;
padding: 0;
}
.easyuse-model-tag {
background-color: var(--comfy-input-bg);
border: 2px solid var(--border-color);
color: var(--input-text);
display: flex;
align-items: center;
gap: 5px;
border-radius: 5px;
padding: 2px 5px;
cursor: pointer;
}
.easyuse-model-tag--selected span::before {
content: "✅";
position: absolute;
background-color: var(--theme-color-light);
left: 0;
top: 0;
right: 0;
bottom: 0;
text-align: center;
}
.easyuse-model-tag:hover {
border: 2px solid var(--theme-color-light);
}
.easyuse-model-tag p {
margin: 0;
}
.easyuse-model-tag span {
text-align: center;
border-radius: 5px;
background-color: var(--theme-color-light);
padding: 2px;
position: relative;
min-width: 20px;
overflow: hidden;
color: #fff;
}
.easyuse-model-metadata .comfy-modal-content {
max-width: 100%;
}
.easyuse-model-metadata label {
margin-right: 1ch;
color: #ccc;
}
.easyuse-model-metadata span {
color: var(--theme-color-light);
}
.easyuse-preview {
max-width:660px;
margin-right: 15px;
position: relative;
}
.easyuse-preview-group{
position: relative;
overflow: hidden;
border-radius:.5rem;
width: 660px;
}
.easyuse-preview-list{
display: flex;
flex-wrap: nowrap;
width: 100%;
transition: all .5s ease-in-out;
}
.easyuse-preview-list.no-transition{
transition: none;
}
.easyuse-preview-slide{
display: flex;
flex-basis: calc(50% - 5px);
flex-grow: 0;
flex-shrink: 0;
position: relative;
justify-content: center;
align-items: center;
padding-right:5px;
padding-left:0;
}
.easyuse-preview-slide:nth-child(even){
padding-left:5px;
padding-right:0;
}
.easyuse-preview-slide-content{
position: relative;
min-height:150px;
width: 100%;
}
.easyuse-preview-slide-content .save{
position: absolute;
right: 6px;
z-index: 12;
bottom: 6px;
display: flex;
align-items: center;
height: 26px;
padding: 0 9px;
color: var(--input-text);
font-size: 12px;
line-height: 26px;
background: rgba(0, 0, 0, .5);
border-radius: 13px;
cursor: pointer;
min-width:80px;
text-align: center;
}
.easyuse-preview-slide-content .save:hover{
filter: brightness(120%);
will-change: auto;
}
.easyuse-preview-slide-content img {
border-radius: 14px;
object-position: center center;
max-width: 100%;
max-height:700px;
border-style: none;
vertical-align: middle;
}
.easyuse-preview button {
position: absolute;
z-index:10;
top: 50%;
display: flex;
align-items: center;
justify-content: center;
width:30px;
height:30px;
border-radius:15px;
border:1px solid rgba(66, 63, 78, .15);
background-color: rgba(66, 63, 78, .5);
color:hsla(0, 0%, 100%, .8);
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .15s;
transform: translateY(-50%);
}
.easyuse-preview button.left{
left:10px;
}
.easyuse-preview button.right{
right:10px;
}
.easyuse-model-detail{
margin-top: 16px;
overflow: hidden;
border: 1px solid var(--border-color);
border-radius: 8px;
width:300px;
}
.easyuse-model-detail-head{
height: 40px;
padding: 0 10px;
font-weight: 500;
font-size: 14px;
font-style: normal;
line-height: 40px;
}
.easyuse-model-detail-body{
box-sizing: border-box;
font-size: 12px;
}
.easyuse-model-detail-item{
display: flex;
justify-content: flex-start;
border-top: 1px solid var(--border-color);
}
.easyuse-model-detail-item-label{
flex-shrink: 0;
width: 88px;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 10px;
border-right: 1px solid var(--border-color);
color: var(--input-text);
font-weight: 400;
}
.easyuse-model-detail-item-value{
display: flex;
flex-wrap: wrap;
padding: 5px 10px 5px 10px;
color: var(--input-text);
}
.easyuse-model-detail-textarea{
border-top:1px solid var(--border-color);
padding:10px;
height:100px;
overflow-y: auto;
font-size: 12px;
}
.easyuse-model-detail-textarea textarea{
width:100%;
height:100%;
border:0;
background-color:transparent;
color: var(--input-text);
}
.easyuse-model-detail-textarea textarea::placeholder{
color:var(--descrip-text);
}
.easyuse-model-detail-textarea.empty{
display: flex;
justify-content: center;
align-items: center;
color: var(--descrip-text);
}
.easyuse-model-notes {
background-color: rgba(0, 0, 0, 0.25);
padding: 5px;
margin-top: 5px;
}
.easyuse-model-notes:empty {
display: none;
}

View File

@@ -0,0 +1,108 @@
.easyuse-prompt-styles{
overflow: auto;
}
.easyuse-prompt-styles .tools{
display:flex;
justify-content:space-between;
height:30px;
padding-bottom:10px;
border-bottom:2px solid var(--border-color);
}
.easyuse-prompt-styles .tools button.delete{
height:30px;
border-radius: 8px;
border: 2px solid var(--border-color);
font-size:11px;
background:var(--comfy-input-bg);
color:var(--error-text);
box-shadow:none;
cursor:pointer;
}
.easyuse-prompt-styles .tools button.delete:hover{
filter: brightness(1.2);
}
.easyuse-prompt-styles .tools textarea.search{
flex:1;
margin-left:10px;
height:20px;
line-height:20px;
border-radius: 8px;
border: 2px solid var(--border-color);
font-size:11px;
background:var(--comfy-input-bg);
color:var(--input-text);
box-shadow:none;
padding:4px 10px;
outline: none;
resize: none;
appearance:none;
}
.easyuse-prompt-styles-list{
list-style: none;
padding: 0;
margin: 0;
min-height: 150px;
height: calc(100% - 40px);
overflow: auto;
/*display: flex;*/
/*flex-wrap: wrap;*/
}
.easyuse-prompt-styles-list.no-top{
height: auto;
}
.easyuse-prompt-styles-tag{
display: inline-block;
vertical-align: middle;
margin-top: 8px;
margin-right: 8px;
padding:4px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border: 2px solid var(--border-color);
font-size:11px;
cursor:pointer;
}
.easyuse-prompt-styles-tag.hide{
display:none;
}
.easyuse-prompt-styles-tag:hover{
filter: brightness(1.2);
}
.easyuse-prompt-styles-tag input{
--ring-color: transparent;
position: relative;
box-shadow: none;
border: 2px solid var(--border-color);
border-radius: 2px;
background: linear-gradient(135deg, var(--comfy-menu-bg) 0%, var(--comfy-input-bg) 60%);
}
.easyuse-prompt-styles-tag input[type=checkbox]:checked{
border: 1px solid var(--theme-color-light);
background-color: var(--theme-color-light);
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
.easyuse-prompt-styles-tag input[type=checkbox]{
color-adjust: exact;
display: inline-block;
flex-shrink: 0;
vertical-align: middle;
appearance: none;
border: 2px solid var(--border-color);
background-origin: border-box;
padding: 0;
width: 1rem;
height: 1rem;
border-radius:4px;
color:var(--theme-color-light);
user-select: none;
}
.easyuse-prompt-styles-tag span{
margin:0 4px;
vertical-align: middle;
}
#show_image_id{
width:128px;
height:128px;
}

View File

@@ -0,0 +1,66 @@
.easyuse-slider{
width:100%;
height:100%;
display: flex;
flex-direction: row;
justify-content: space-between;
position: relative;
}
.easyuse-slider-item{
height: inherit;
min-width: 25px;
justify-content: center;
display: flex;
flex-direction: column;
align-items: center;
}
.easyuse-slider-item.positive .easyuse-slider-item-label{
color: var(--success-color);
}
.easyuse-slider-item.negative .easyuse-slider-item-label{
color: var(--error-color);
}
.easyuse-slider-item-input{
height:15px;
font-size: 10px;
color: var(--input-text);
}
.easyuse-slider-item-label{
height:15px;
border: none;
color: var(--descrip-text);
font-size: 8px;
}
.easyuse-slider-item-scroll {
width: 5px;
height: calc(100% - 30px);
background: var(--comfy-input-bg);
border-radius: 10px;
position: relative;
}
.easyuse-slider-item-bar{
width: 10px;
height: 10px;
background: linear-gradient(to bottom, var(--input-text), var(--descrip-text));
border-radius:100%;
box-shadow: 0 2px 10px var(--bg-color);
position: absolute;
top: 0;
left:-2.5px;
cursor: pointer;
z-index:1;
}
.easyuse-slider-item-area{
width: 100%;
border-radius:20px;
position: absolute;
bottom: 0;
background: var(--input-text);
z-index:0;
}
.easyuse-slider-item.positive .easyuse-slider-item-area{
background: var(--success-color);
}
.easyuse-slider-item.negative .easyuse-slider-item-area{
background: var(--error-color);
}

View File

@@ -0,0 +1,10 @@
:root {
/*--theme-color:#3f3eed;*/
/*--theme-color-light: #008ecb;*/
--theme-color:#236692;
--theme-color-light: #3485bb;
--success-color: #52c41a;
--error-color: #ff4d4f;
--warning-color: #faad14;
--font-family: Inter, -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
}

View File

@@ -0,0 +1,110 @@
.easyuse-toast-container{
position: fixed;
z-index: 99999;
top: 0;
left: 0;
width: 100%;
height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
padding:10px 0;
}
.easyuse-toast-container > div {
position: relative;
height: fit-content;
padding: 4px;
margin-top: -100px; /* re-set by JS */
opacity: 0;
transition: all 0.33s ease-in-out;
z-index: 3;
}
.easyuse-toast-container > div:last-child {
z-index: 2;
}
.easyuse-toast-container > div:not(.-show) {
z-index: 1;
}
.easyuse-toast-container > div.-show {
opacity: 1;
margin-top: 0px !important;
}
.easyuse-toast-container > div.-show {
opacity: 1;
transform: translateY(0%);
}
.easyuse-toast-container > div > div {
position: relative;
background: var(--comfy-menu-bg);
color: var(--input-text);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: fit-content;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.88);
padding: 9px 12px;
border-radius: 8px;
font-family: Arial, sans-serif;
font-size: 14px;
pointer-events: all;
}
.easyuse-toast-container > div > div > span {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.easyuse-toast-container > div > div > span svg {
width: 16px;
height: auto;
margin-right: 8px;
}
.easyuse-toast-container > div > div > span svg[data-icon=info-circle]{
fill: var(--theme-color-light);
}
.easyuse-toast-container > div > div > span svg[data-icon=check-circle]{
fill: var(--success-color);
}
.easyuse-toast-container > div > div > span svg[data-icon=close-circle]{
fill: var(--error-color);
}
.easyuse-toast-container > div > div > span svg[data-icon=exclamation-circle]{
fill: var(--warning-color);
}
/*rotate animation*/
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.easyuse-toast-container > div > div > span svg[data-icon=loading]{
fill: var(--theme-color);
animation: rotate 1s linear infinite;
}
.easyuse-toast-container a {
cursor: pointer;
text-decoration: underline;
color: var(--theme-color-light);
margin-left: 4px;
display: inline-block;
line-height: 1;
}
.easyuse-toast-container a:hover {
color: var(--theme-color-light);
text-decoration: none;
}

View File

@@ -0,0 +1,230 @@
.easyuse-toolbar{
background: rgba(15,15,15,.5);
backdrop-filter: blur(4px) brightness(120%);
border-radius:0 12px 12px 0;
min-width:50px;
height:24px;
position: fixed;
bottom:85px;
left:0px;
display: flex;
align-items: center;
z-index:10000;
}
.easyuse-toolbar.disable-render-info{
bottom: 55px;
}
.easyuse-toolbar-item{
border-radius:20px;
height: 20px;
width:20px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease-in-out;
margin-left:2.5px;
}
.easyuse-toolbar-icon{
width: 14px;
height: 14px;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
color:white;
transition: all 0.3s ease-in-out;
}
.easyuse-toolbar-icon svg{
width: 14px;
height: 14px;
}
.easyuse-toolbar-tips{
visibility: hidden;
opacity: 0;
position: absolute;
top: -25px;
left: 0;
color: var(--descrip-text);
padding: 2px 5px;
border-radius: 5px;
font-size: 11px;
min-width:100px;
transition: all 0.3s ease-in-out;
}
.easyuse-toolbar-item:hover{
background:rgba(12,12,12,1);
}
.easyuse-toolbar-item:hover .easyuse-toolbar-tips{
opacity: 1;
visibility: visible;
}
.easyuse-toolbar-item:hover .easyuse-toolbar-icon.group{
color:var(--warning-color);
}
.easyuse-toolbar-item:hover .easyuse-toolbar-icon.rocket{
color:var(--theme-color-light);
}
.easyuse-toolbar-item:hover .easyuse-toolbar-icon.question{
color:var(--success-color);
}
.easyuse-guide-dialog{
max-width: 300px;
font-family: var(--font-family);
position: absolute;
z-index:100;
left:0;
bottom:140px;
background: rgba(25,25,25,.85);
backdrop-filter: blur(8px) brightness(120%);
border-radius:0 12px 12px 0;
padding:10px;
transition: .5s all ease-in-out;
visibility: visible;
opacity: 1;
transform: translateX(0%);
}
.easyuse-guide-dialog.disable-render-info{
bottom:110px;
}
.easyuse-guide-dialog-top{
display: flex;
justify-content: space-between;
align-items: center;
}
.easyuse-guide-dialog-top .icon{
width: 12px;
height:12px;
}
.easyuse-guide-dialog.hidden{
opacity: 0;
transform: translateX(-50%);
visibility: hidden;
}
.easyuse-guide-dialog .closeBtn{
float: right;
color: var(--input-text);
border-radius:30px;
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
cursor: pointer;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
}
.easyuse-guide-dialog .closeBtn:hover{
filter:brightness(120%);
}
.easyuse-guide-dialog-title{
color:var(--input-text);
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.easyuse-guide-dialog-remark{
color: var(--input-text);
font-size: 12px;
margin-top: 5px;
}
.easyuse-guide-dialog-content{
max-height: 600px;
overflow: auto;
}
.easyuse-guide-dialog a, .easyuse-guide-dialog a:visited{
color: var(--theme-color-light);
cursor: pointer;
}
.easyuse-guide-dialog-note{
margin-top: 20px;
color:white;
}
.easyuse-guide-dialog p{
margin:4px 0;
font-size: 12px;
font-weight: 300;
}
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
margin-top: 12px;
margin-bottom: 8px;
font-weight: 600;
line-height: 1.25;
padding-bottom: 5px;
border-bottom: 1px solid var(--border-color);
color: var(--input-text);
}
.markdown-body h1{
font-size: 18px;
}
.markdown-body h2{
font-size: 16px;
}
.markdown-body h3{
font-size: 14px;
}
.markdown-body h4{
font-size: 13px;
}
.markdown-body table {
display: block;
/*width: 100%;*/
/*width: max-content;*/
max-width: 300px;
overflow: auto;
color:var(--input-text);
box-sizing: border-box;
border: 1px solid var(--border-color);
text-align: left;
width: 100%;
}
.markdown-body table th, .markdown-body table td {
padding: 6px 13px;
font-size: 12px;
margin:0;
border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.markdown-body table td {
font-size: 12px;
}
.markdown-body table th:last-child, .markdown-body table td:last-child{
border-right: none;
}
.markdown-body table tr:last-child td{
border-bottom: none;
}
.markdown-body table th{
font-weight: bold;
width: auto;
min-width: 70px;
}
.markdown-body table th:last-child{
width:100%;
}
.markdown-body .warning{
color:var(--warning-color)
}
.markdown-body .error{
color:var(--error-color)
}
.markdown-body .success{
color:var(--success-color)
}
.markdown-body .link{
color:var(--theme-color-light)
}
#comfyui-menu-monitor{
width:120px;
}
#comfyui-menu-monitor #crystools-monitor-container{
margin:0 auto!important;
}
#comfyui-menu-monitor #crystools-monitor-container > div{
margin:2px 0!important;
}
#comfyui-menu-monitor #crystools-monitor-container > div > div > div{
padding:0 4px!important;
}

View File

@@ -0,0 +1,101 @@
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "easy bookmark",
registerCustomNodes() {
class Bookmark {
type = 'easy bookmark'
title = "🔖";
slot_start_y = -20;
___collapsed_width = 0;
get _collapsed_width() {
return this.___collapsed_width;
}
set _collapsed_width(width){
const canvas = app.canvas ;
const ctx = canvas.canvas.getContext('2d');
if(ctx){
const oldFont = ctx.font;
ctx.font = canvas.title_text_font;
this.___collapsed_width = 40 + ctx.measureText(this.title).width;
ctx.font = oldFont;
}
}
isVirtualNode = true;
serialize_widgets = true;
keypressBound = null;
constructor() {
this.addWidget('text', 'shortcut_key', '1', (value) => {
value = value.trim()[0] || '1';
if(value !== ''){
this.title = "🔖 " + value;
}
},{
y: 8,
});
this.addWidget('number', 'zoom', 1, (value) => {}, {
y: 8 + LiteGraph.NODE_WIDGET_HEIGHT + 4,
max: 2,
min: 0.5,
precision: 2,
});
this.keypressBound = this.onKeypress.bind(this);
}
onAdded(){
setTimeout(_=>{
const value = this.widgets[0].value
if(value){
this.title = "🔖 " + value;
}
},1)
window.addEventListener("keydown", this.keypressBound);
}
onRemoved() {
window.removeEventListener("keydown", this.keypressBound);
}
onKeypress(event){
const target = event.target;
if (['input','textarea'].includes(target.localName)) {
return;
}
if (this.widgets[0] && event.key.toLocaleLowerCase() === this.widgets[0].value.toLocaleLowerCase()) {
this.canvasToBookmark();
}
}
canvasToBookmark() {
const canvas = app.canvas;
// ComfyUI seemed to break us again, but couldn't repro. No reason to not check, I guess.
// https://github.com/rgthree/rgthree-comfy/issues/71
if (canvas?.ds?.offset) {
canvas.ds.offset[0] = -this.pos[0] + 16;
canvas.ds.offset[1] = -this.pos[1] + 40;
}
if (canvas?.ds?.scale != null) {
canvas.ds.scale = Number(this.widgets[1].value || 1);
}
canvas.setDirty(true, true);
}
}
LiteGraph.registerNodeType(
"easy bookmark",
Object.assign(Bookmark,{
title: "Bookmark 🔖",
})
);
Bookmark.category = "EasyUse/Util"
}
})

View File

@@ -0,0 +1,218 @@
let activeDropdown = null;
export function removeDropdown() {
if (activeDropdown) {
activeDropdown.removeEventListeners();
activeDropdown.dropdown.remove();
activeDropdown = null;
}
}
export function createDropdown(inputEl, suggestions, onSelect, isDict = false) {
removeDropdown();
new Dropdown(inputEl, suggestions, onSelect, isDict);
}
class Dropdown {
constructor(inputEl, suggestions, onSelect, isDict = false) {
this.dropdown = document.createElement('ul');
this.dropdown.setAttribute('role', 'listbox');
this.dropdown.classList.add('easy-dropdown');
this.selectedIndex = -1;
this.inputEl = inputEl;
this.suggestions = suggestions;
this.onSelect = onSelect;
this.isDict = isDict;
this.focusedDropdown = this.dropdown;
this.buildDropdown();
this.onKeyDownBound = this.onKeyDown.bind(this);
this.onWheelBound = this.onWheel.bind(this);
this.onClickBound = this.onClick.bind(this);
this.addEventListeners();
}
buildDropdown() {
if (this.isDict) {
this.buildNestedDropdown(this.suggestions, this.dropdown);
} else {
this.suggestions.forEach((suggestion, index) => {
this.addListItem(suggestion, index, this.dropdown);
});
}
const inputRect = this.inputEl.getBoundingClientRect();
this.dropdown.style.top = (inputRect.top + inputRect.height - 10) + 'px';
this.dropdown.style.left = inputRect.left + 'px';
document.body.appendChild(this.dropdown);
activeDropdown = this;
}
buildNestedDropdown(dictionary, parentElement) {
let index = 0;
Object.keys(dictionary).forEach((key) => {
const item = dictionary[key];
if (typeof item === "object" && item !== null) {
const nestedDropdown = document.createElement('ul');
nestedDropdown.setAttribute('role', 'listbox');
nestedDropdown.classList.add('easy-nested-dropdown');
const parentListItem = document.createElement('li');
parentListItem.classList.add('folder');
parentListItem.textContent = key;
parentListItem.appendChild(nestedDropdown);
parentListItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement));
parentElement.appendChild(parentListItem);
this.buildNestedDropdown(item, nestedDropdown);
index = index + 1;
} else {
const listItem = document.createElement('li');
listItem.classList.add('item');
listItem.setAttribute('role', 'option');
listItem.textContent = key;
listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement));
listItem.addEventListener('mousedown', this.onMouseDown.bind(this, key));
parentElement.appendChild(listItem);
index = index + 1;
}
});
}
addListItem(item, index, parentElement) {
const listItem = document.createElement('li');
listItem.setAttribute('role', 'option');
listItem.textContent = item;
listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index));
listItem.addEventListener('mousedown', this.onMouseDown.bind(this, item));
parentElement.appendChild(listItem);
}
addEventListeners() {
document.addEventListener('keydown', this.onKeyDownBound);
this.dropdown.addEventListener('wheel', this.onWheelBound);
document.addEventListener('click', this.onClickBound);
}
removeEventListeners() {
document.removeEventListener('keydown', this.onKeyDownBound);
this.dropdown.removeEventListener('wheel', this.onWheelBound);
document.removeEventListener('click', this.onClickBound);
}
onMouseOver(index, parentElement) {
if (parentElement) {
this.focusedDropdown = parentElement;
}
this.selectedIndex = index;
this.updateSelection();
}
onMouseOut() {
this.selectedIndex = -1;
this.updateSelection();
}
onMouseDown(suggestion, event) {
event.preventDefault();
this.onSelect(suggestion);
this.dropdown.remove();
this.removeEventListeners();
}
onKeyDown(event) {
const enterKeyCode = 13;
const escKeyCode = 27;
const arrowUpKeyCode = 38;
const arrowDownKeyCode = 40;
const arrowRightKeyCode = 39;
const arrowLeftKeyCode = 37;
const tabKeyCode = 9;
const items = Array.from(this.focusedDropdown.children);
const selectedItem = items[this.selectedIndex];
if (activeDropdown) {
if (event.keyCode === arrowUpKeyCode) {
event.preventDefault();
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
this.updateSelection();
}
else if (event.keyCode === arrowDownKeyCode) {
event.preventDefault();
this.selectedIndex = Math.min(items.length - 1, this.selectedIndex + 1);
this.updateSelection();
}
else if (event.keyCode === arrowRightKeyCode) {
event.preventDefault();
if (selectedItem && selectedItem.classList.contains('folder')) {
const nestedDropdown = selectedItem.querySelector('.easy-nested-dropdown');
if (nestedDropdown) {
this.focusedDropdown = nestedDropdown;
this.selectedIndex = 0;
this.updateSelection();
}
}
}
else if (event.keyCode === arrowLeftKeyCode && this.focusedDropdown !== this.dropdown) {
const parentDropdown = this.focusedDropdown.closest('.easy-dropdown, .easy-nested-dropdown').parentNode.closest('.easy-dropdown, .easy-nested-dropdown');
if (parentDropdown) {
this.focusedDropdown = parentDropdown;
this.selectedIndex = Array.from(parentDropdown.children).indexOf(this.focusedDropdown.parentNode);
this.updateSelection();
}
}
else if ((event.keyCode === enterKeyCode || event.keyCode === tabKeyCode) && this.selectedIndex >= 0) {
event.preventDefault();
if (selectedItem.classList.contains('item')) {
this.onSelect(items[this.selectedIndex].textContent);
this.dropdown.remove();
this.removeEventListeners();
}
const nestedDropdown = selectedItem.querySelector('.easy-nested-dropdown');
if (nestedDropdown) {
this.focusedDropdown = nestedDropdown;
this.selectedIndex = 0;
this.updateSelection();
}
}
else if (event.keyCode === escKeyCode) {
this.dropdown.remove();
this.removeEventListeners();
}
}
}
onWheel(event) {
const top = parseInt(this.dropdown.style.top);
if (localStorage.getItem("Comfy.Settings.Comfy.InvertMenuScrolling")) {
this.dropdown.style.top = (top + (event.deltaY < 0 ? 10 : -10)) + "px";
} else {
this.dropdown.style.top = (top + (event.deltaY < 0 ? -10 : 10)) + "px";
}
}
onClick(event) {
if (!this.dropdown.contains(event.target) && event.target !== this.inputEl) {
this.dropdown.remove();
this.removeEventListeners();
}
}
updateSelection() {
Array.from(this.focusedDropdown.children).forEach((li, index) => {
if (index === this.selectedIndex) {
li.classList.add('selected');
} else {
li.classList.remove('selected');
}
});
}
}

View File

@@ -0,0 +1,101 @@
import {getLocale} from './utils.js'
const locale = getLocale()
const zhCN = {
"Workflow created by": "工作流创建者",
"Watch more video content": "观看更多视频内容",
"Workflow Guide":"工作流指南",
// ExtraMenu
"💎 View Checkpoint Info...": "💎 查看 Checkpoint 信息...",
"💎 View Lora Info...": "💎 查看 Lora 信息...",
"🔃 Reload Node": "🔃 刷新节点",
// ModelInfo
"Updated At:": "最近更新:",
"Created At:": "首次发布:",
"✏️ Edit": "✏️ 编辑",
"💾 Save": "💾 保存",
"No notes": "当前还没有备注内容",
"Saving Notes...": "正在保存备注...",
"Type your notes here":"在这里输入备注内容",
"ModelName":"模型名称",
"Models Required":"所需模型",
"Download Model": "下载模型",
"Source Url": "模型源地址",
"Notes": "备注",
"Type": "类型",
"Trained Words": "训练词",
"BaseModel": "基础算法",
"Details": "详情",
"Description": "描述",
"Download": "下载量",
"Source": "来源",
"Saving Preview...": "正在保存预览图...",
"Saving Succeed":"保存成功",
"Clean SuccessFully":"清理成功",
"Clean Failed": "清理失败",
"Saving Failed":"保存失败",
"No COMBO link": "沒有找到COMBO连接",
"Reboot ComfyUI":"重启ComfyUI",
"Are you sure you'd like to reboot the server?": "是否要重启ComfyUI",
// GroupMap
"Groups Map": "管理组",
"Cleanup Of GPU Usage": "清理GPU占用",
"Please stop all running tasks before cleaning GPU": "请在清理GPU之前停止所有运行中的任务",
"Always": "启用中",
"Bypass": "已忽略",
"Never": "已停用",
"Auto Sorting": "自动排序",
"Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never": "点击`启用中/已忽略`可设置组模式, 长按可停用该组节点",
// Quick
"Enable ALT+1~9 to paste nodes from nodes template (ComfyUI-Easy-Use)": "启用ALT1~9从节点模板粘贴到工作流 (ComfyUI-Easy-Use)",
"Enable process bar in queue button (ComfyUI-Easy-Use)": "启用提示词队列进度显示条 (ComfyUI-Easy-Use",
"Enable ContextMenu Auto Nest Subdirectories (ComfyUI-Easy-Use)": "启用上下文菜单自动嵌套子目录 (ComfyUI-Easy-Use)",
"Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)": "启用工具栏固定在左下角 (ComfyUI-Easy-Use)",
"Too many thumbnails, have closed the display": "模型缩略图太多啦,为您关闭了显示",
// selector
"Empty All": "清空所有",
"🔎 Type here to search styles ...": "🔎 在此处输入以搜索样式 ...",
// account
"Loading UserInfo...": "正在获取用户信息...",
"Please set the APIKEY first": "请先设置APIKEY",
"Setting APIKEY": "设置APIKEY",
"Save Account Info": "保存账号信息",
"Choose": "选择",
"Delete": "删除",
"Edit": "编辑",
"At least one account is required": "删除失败: 至少需要一个账户",
"APIKEY is not Empty": "APIKEY 不能为空",
"Add Account": "添加账号",
"Getting Your APIKEY": "获取您的APIKEY",
// choosers
"Choose Selected Images": "选择选中的图片",
"Choose images to continue": "选择图片以继续",
// seg
"Background": "背景",
"Hat": "帽子",
"Hair": "头发",
"Body": "身体",
"Face": "脸部",
"Clothes": "衣服",
"Others": "其他",
"Glove": "手套",
"Sunglasses": "太阳镜",
"Upper-clothes": "上衣",
"Dress": "连衣裙",
"Coat": "外套",
"Socks": "袜子",
"Pants": "裤子",
"Jumpsuits": "连体衣",
"Scarf": "围巾",
"Skirt": "裙子",
"Left-arm": "左臂",
"Right-arm": "右臂",
"Left-leg": "左腿",
"Right-leg": "右腿",
"Left-shoe": "左鞋",
"Right-shoe": "右鞋",
}
export const $t = (key) => {
const cn = zhCN[key]
return locale === 'zh-CN' && cn ? cn : key
}

View File

@@ -0,0 +1,25 @@
export const logoIcon = `<svg width="347px" height="300px" viewBox="0 0 347 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g id="icon" transform="translate(-17, -49)">
<g id="easy" transform="translate(17, 49)">
<rect id="矩形" fill="#3A3A3A" x="7" y="106" width="328" height="119"></rect>
<rect id="矩形" fill="#292929" x="7" y="69" width="328" height="39"></rect>
<path d="M328,57 C338.49341,57 347,65.5065898 347,76 L347,222 C347,232.49341 338.49341,241 328,241 L19,241 C8.50658975,241 7.74796342e-15,232.49341 0,222 L0,76 C-1.28507213e-15,65.5065898 8.50658975,57 19,57 L328,57 Z M318.532203,73.5128205 L30.820339,73.5128205 C24.192922,73.5128205 18.820339,78.8854035 18.820339,85.5128205 L18.820339,212.487179 C18.820339,219.114596 24.192922,224.487179 30.820339,224.487179 L318.532203,224.487179 C325.15962,224.487179 330.532203,219.114596 330.532203,212.487179 L330.532203,85.5128205 C330.532203,78.8854035 325.15962,73.5128205 318.532203,73.5128205 Z" id="矩形-2" fill="#000000"></path>
<path d="M236.357276,240.344534 C229.387569,244.108454 224.824659,250.430287 219.628567,256.080285 C212.724768,251.899851 209.926058,252.06928 204.312161,257.969893 C188.040097,275.069315 168.819854,284.472644 144.661264,281.434681 C143.99866,281.282901 143.336055,281.132297 142.674627,280.981693 L142.31096,280.918157 C135.57546,274.77752 128.299755,269.300481 121.68077,262.972765 C102.148644,244.300238 96.1628402,211.734505 108.227423,187.366093 C110.124615,183.533931 111.676969,179.508807 113.110455,175.473094 C118.554876,160.139738 116.691815,143.908641 118.645499,128.138769 C120.1496,115.999861 120.390868,103.703291 121.145272,91.4726095 C121.250018,89.7747862 121.798461,88.4475896 123.27667,87.5616153 C126.008296,95.5753884 126.567332,103.982143 127.811334,112.272416 C129.555526,123.897152 134.253781,128.17289 145.804051,128.989445 C145.43803,132.846316 147.270491,135.554833 150.727098,136.730249 C153.974214,137.835069 156.603448,136.356092 159.056145,133.892307 C189.877265,102.947918 220.789008,72.0929508 251.656029,41.1956258 C252.609332,40.2414091 253.343729,39.0706994 254.17934,38 C259.396616,43.3240821 259.109448,47.7339516 252.918861,53.9345953 C228.40955,78.485379 203.843747,102.979686 179.349736,127.544589 C174.94571,131.961518 174.769173,137.358549 178.597686,141.040108 C182.332046,144.632245 187.280985,144.378101 191.647349,140.241201 C194.491959,137.546804 197.127078,134.630031 199.99405,131.960342 C204.506352,127.755199 209.768351,127.501055 213.634526,131.137903 C217.616038,134.882998 217.557192,140.563588 213.322642,145.277018 C211.099445,147.751393 208.627918,150.002215 206.287029,152.371872 C201.132129,157.590061 200.424801,162.74942 204.299215,166.72748 C208.353696,170.890265 213.268504,170.338443 218.59288,165.120255 C220.272341,163.475378 221.841173,161.69637 223.650095,160.20798 C227.582177,156.97235 233.243151,156.980586 236.463198,160.338582 C240.098697,164.131917 239.948051,168.521784 237.31764,172.708101 C235.991254,174.818908 233.92694,176.474374 232.149795,178.291033 C227.054918,183.502163 226.346414,188.706232 230.231419,192.654877 C234.312969,196.80472 238.906479,196.2729 244.5192,191.001765 C250.03659,185.82005 255.291527,185.377651 259.431924,189.747517 C263.298098,193.82794 262.808501,199.286154 257.8984,204.298439 C251.179377,211.159151 244.326185,217.886908 237.556555,224.69938 C231.602528,230.69059 231.385975,233.755614 236.357276,240.344534" id="Fill-1" fill="#F7CABB"></path>
<path d="M142,281 C99.8323615,275.972516 72.8905321,229.508012 90.4429765,191.002025 C97.205854,176.166768 98.8592352,160.761783 99.9233733,144.957755 C101.420401,122.72429 103.161293,100.507306 104.785504,78.2820813 C105.081876,74.2292532 106.526397,70.816779 110.840123,70.1046207 C114.869146,69.4383701 117.276292,72.0374536 118.826993,75.5335034 C120.557384,79.4344829 122.314612,83.3248683 123.130218,87.5577959 C121.663528,88.442991 121.120957,89.7719608 121.017111,91.4705466 C120.268013,103.705544 120.028816,116.007637 118.537622,128.153173 C116.600704,143.930128 118.447777,160.167337 113.050077,175.508757 C111.630059,179.545105 110.091026,183.573214 108.210115,187.407097 C96.2478964,211.785276 102.182333,244.365636 121.546846,263.046548 C128.109031,269.377106 135.3223,274.857782 142,281" id="Fill-3" fill="#F2AB9B"></path>
<path d="M146.046133,128.926434 C150.920728,128.31052 154.622981,125.725088 158.028632,122.311802 C185.262118,95.0101969 212.557739,67.7694807 239.855703,40.5322774 C245.237922,35.1623528 248.464205,34.7232509 254,38.3742375 C253.166465,39.4397916 252.434924,40.6048756 251.4865,41.5545068 C220.738335,72.304526 189.947966,103.01005 159.246695,133.806906 C156.802363,136.257681 154.183353,137.729551 150.950036,136.630039 C147.505698,135.460272 145.680362,132.764771 146.046133,128.926434" id="Fill-5" fill="#F0A999"></path>
<path d="M151.112706,104.082785 C177.053192,78.2027637 202.891282,52.2205287 228.952997,26.4638686 C244.25247,11.3433004 268.927711,17.3551067 274.531279,37.5593344 C277.54433,48.4268757 274.452422,57.971514 266.398395,65.9606254 C252.071102,80.1741945 237.837967,94.4829279 223.515381,108.702371 C222.137146,110.069919 220.410526,111.087356 218.246073,112.71925 C229.562669,119.217452 234.902596,127.95613 234.371781,139.989141 C248.189445,143.544296 256.015139,152.051525 257.33688,166.30034 C257.462816,167.662013 259.216506,169.49011 260.624166,170.02585 C281.015224,177.772939 286.435185,200.143625 271.473503,216.024334 C264.220995,223.722078 256.614218,231.092033 249.026273,238.465513 C244.58673,242.779633 239.231503,242.893595 235.482844,239.124615 C231.700052,235.321563 231.975464,230.303696 236.490333,225.689984 C243.34267,218.688938 250.382146,211.871171 257.218005,204.856026 C258.813981,203.218258 261.014921,201.09292 261.001974,199.20608 C260.978434,195.842429 260.489991,191.269837 258.313767,189.474637 C256.110474,187.655939 251.585012,188.159958 248.164729,188.510069 C246.572284,188.673376 245.166978,190.850409 243.713416,192.155689 C238.92549,196.454536 234.024574,196.671887 230.19235,192.72785 C226.54491,188.976493 226.92978,183.714253 231.229263,179.166334 C232.842893,177.459249 234.637777,175.914295 236.173727,174.141418 C239.944749,169.788527 239.964757,163.749698 236.333795,160.393097 C232.76639,157.094064 227.265218,157.331387 223.065778,161.08627 C221.022552,162.912017 219.226492,165.012683 217.172674,166.825506 C212.995596,170.515771 208.141759,170.56629 204.515505,167.099251 C200.831579,163.574643 200.824518,158.196091 204.693228,153.987709 C207.475592,150.961246 210.60281,148.247298 213.346334,145.189113 C217.446908,140.617696 217.513996,135.200374 213.771221,131.512459 C210.002554,127.798697 204.702644,128.006649 200.152466,132.204457 C197.418357,134.728077 194.903166,137.485495 192.212606,140.058459 C187.331698,144.723865 182.149486,145.008183 178.271359,140.905539 C174.618035,137.040219 174.962888,131.914264 179.489527,127.381619 C203.306754,103.535296 227.179299,79.7453672 251.030658,55.9342906 C252.278249,54.6889291 253.59999,53.5023111 254.734592,52.1606104 C258.53033,47.6737844 258.68216,42.3786484 255.200674,38.9304069 C251.353149,35.1203057 245.99439,35.1109068 241.586626,39.3721578 C233.557316,47.1345194 225.738684,55.113057 217.832955,63.0023045 C198.000959,82.7965028 178.090106,102.514335 158.388753,122.437769 C152.951138,127.937332 146.992123,130.706499 139.410062,128.300366 C131.778569,125.877786 128.770226,120.109178 127.524988,112.464303 C125.807785,101.913976 123.226683,91.4999347 120.900984,81.0529967 C120.526706,79.3682341 119.960582,77.3874044 118.783609,76.2912513 C116.41907,74.0895462 113.255367,70.6072336 111.009702,71.0008148 C108.678118,71.4084945 105.634466,75.6239256 105.335515,78.4330381 C103.619488,94.562819 102.648485,110.771316 101.380885,126.949267 C100.28277,140.976032 99.7966796,155.096786 97.7334458,168.982566 C96.5811892,176.734354 93.2327008,184.294638 90.0642893,191.589402 C79.583344,215.716518 83.954622,239.009477 102.147095,258.025912 C117.64783,274.228535 135.84148,283.188089 158.615909,281.527998 C173.230384,280.463567 185.780448,274.550449 196.662741,264.951767 C200.186598,261.844238 203.329116,258.291433 206.960078,255.324888 C210.74287,252.234981 214.919947,252.206785 218.707446,255.52579 C222.194818,258.579276 222.709155,264.037719 219.548982,267.699786 C185.86519,306.748918 134.584473,309.856447 99.0551866,279.924302 C83.9922852,267.234538 71.9577355,252.377141 67.8442146,232.265728 C64.8076241,217.420079 65.1371765,202.689567 71.7458803,189.240838 C79.6009986,173.252042 81.3229102,156.469033 82.4116103,139.245449 C83.7674833,117.777062 85.484687,96.3286476 87.2701552,74.8896322 C88.1693626,64.0878837 96.7024174,54.8310883 107.046834,52.7539194 C117.546611,50.6462038 128.357108,55.6206005 133.80414,65.587017 C140.171564,77.238196 142.938628,90.0184245 144.667601,103.055949 C144.892403,104.747761 145.127798,106.439573 145.530322,109.399069 C148.033744,107.018784 149.607357,105.584268 151.112706,104.082785 Z" id="Fill-1" fill="#000000"></path>
<rect id="矩形" fill="#000000" x="154" y="0" width="19" height="36" rx="9"></rect>
<path d="M123.077967,7.43459974 L123.860245,7.03887616 C128.2773,4.80446126 133.669673,6.55651248 135.929845,10.9604444 L143.86657,26.4251086 C146.136108,30.8472904 144.391045,36.272001 139.968863,38.5415388 C139.953276,38.5495382 139.937666,38.557492 139.922033,38.5654003 L139.139755,38.9611238 C134.7227,41.1955387 129.330327,39.4434875 127.070155,35.0395556 L119.13343,19.5748914 C116.863892,15.1527096 118.608955,9.72799898 123.031137,7.45846116 C123.046724,7.45046179 123.062334,7.44250795 123.077967,7.43459974 Z" id="矩形备份" fill="#000000"></path>
<path d="M203.922033,7.43459974 L203.139755,7.03887616 C198.7227,4.80446126 193.330327,6.55651248 191.070155,10.9604444 L183.13343,26.4251086 C180.863892,30.8472904 182.608955,36.272001 187.031137,38.5415388 C187.046724,38.5495382 187.062334,38.557492 187.077967,38.5654003 L187.860245,38.9611238 C192.2773,41.1955387 197.669673,39.4434875 199.929845,35.0395556 L207.86657,19.5748914 C210.136108,15.1527096 208.391045,9.72799898 203.968863,7.45846116 C203.953276,7.45046179 203.937666,7.44250795 203.922033,7.43459974 Z" id="矩形备份" fill="#000000"></path>
<circle id="椭圆形" fill="#B014BD" cx="37" cy="127" r="10"></circle>
<circle id="椭圆形备份-2" fill="#357CFF" cx="37" cy="156" r="10"></circle>
<circle id="椭圆形备份" fill="#B014BD" cx="308" cy="127" r="10"></circle>
<circle id="椭圆形备份-3" fill="#357CFF" cx="308" cy="159" r="10"></circle>
</g>
</g>
</svg>`
export const quesitonIcon = `<svg t="1714564780771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1489" width="200" height="200" data-spm-anchor-id="a313x.search_index.0.i2.5a663a81pw6qup"><path d="M514.048 54.272q95.232 0 178.688 36.352t145.92 98.304 98.304 145.408 35.84 178.688-35.84 178.176-98.304 145.408-145.92 98.304-178.688 35.84-178.176-35.84-145.408-98.304-98.304-145.408-35.84-178.176 35.84-178.688 98.304-145.408 145.408-98.304 178.176-36.352zM515.072 826.368q26.624 0 44.544-17.92t17.92-43.52q0-26.624-17.92-44.544t-44.544-17.92-44.544 17.92-17.92 44.544q0 25.6 17.92 43.52t44.544 17.92zM567.296 574.464q-1.024-16.384 20.48-34.816t48.128-40.96 49.152-50.688 24.576-65.024q2.048-39.936-8.192-74.752t-33.792-59.904-60.928-39.936-87.552-14.848q-62.464 0-103.936 22.016t-67.072 53.248-35.84 64.512-9.216 55.808q1.024 26.624 16.896 38.912t34.304 12.8 33.792-10.24 15.36-31.232q0-12.288 7.68-30.208t20.992-34.304 32.256-27.648 42.496-11.264q46.08 0 73.728 23.04t25.6 57.856q0 17.408-10.24 32.256t-26.112 28.672-33.792 27.648-33.792 28.672-26.624 32.256-11.776 37.888l1.024 38.912q0 15.36 14.336 29.184t37.888 14.848q23.552-1.024 37.376-15.36t12.8-32.768l0-24.576z" p-id="1490" fill="currentColor"></path></svg>`
export const rocketIcon = `<svg t="1714565020764" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7999" width="200" height="200"><path d="M810.438503 379.664884l-71.187166-12.777183C737.426025 180.705882 542.117647 14.602496 532.991087 7.301248c-12.777184-10.951872-32.855615-10.951872-47.45811 0-9.12656 7.301248-204.434938 175.229947-206.26025 359.586453l-67.536542 10.951871c-18.253119 3.650624-31.030303 18.253119-31.030303 36.506239v189.832442c0 10.951872 5.475936 21.903743 12.777184 27.379679 7.301248 5.475936 14.602496 9.12656 23.729055 9.12656h5.475936l133.247772-23.729055c40.156863 47.458111 91.265597 73.012478 151.500891 73.012477 60.235294 0 111.344029-27.379679 151.500891-74.837789l136.898396 23.729055h5.475936c9.12656 0 16.427807-3.650624 23.729055-9.12656 9.12656-7.301248 12.777184-16.427807 12.777184-27.379679V412.520499c1.825312-14.602496-10.951872-29.204991-27.379679-32.855615zM620.606061 766.631016H401.568627c-20.078431 0-36.506239 16.427807-36.506238 36.506239v109.518716c0 14.602496 9.12656 29.204991 23.729055 34.680927 14.602496 5.475936 31.030303 1.825312 40.156863-9.126559l16.427807-18.25312 32.855615 80.313726c5.475936 14.602496 18.253119 23.729055 34.680927 23.729055 16.427807 0 27.379679-9.12656 34.680927-23.729055l32.855615-80.313726 16.427807 18.25312c10.951872 10.951872 25.554367 14.602496 40.156863 9.126559 14.602496-5.475936 23.729055-18.253119 23.729055-34.680927v-109.518716c-3.650624-20.078431-20.078431-36.506239-40.156862-36.506239z" fill="currentColor" p-id="8000"></path></svg>`
export const groupIcon = `<svg t="1714565543756" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22538" width="200" height="200"><path d="M871.616 64H152.384c-31.488 0-60.416 25.28-60.416 58.24v779.52c0 32.896 26.24 58.24 60.352 58.24h719.232c34.112 0 60.352-25.344 60.352-58.24V122.24c0.128-32.96-28.8-58.24-60.288-58.24zM286.272 512c-23.616 0-44.672-20.224-44.672-43.008 0-22.784 20.992-43.008 44.608-43.008 23.616 0 44.608 20.224 44.608 43.008A43.328 43.328 0 0 1 286.272 512z m0-202.496c-23.616 0-44.608-20.224-44.608-43.008 0-22.784 20.992-43.008 44.608-43.008 23.616 0 44.608 20.224 44.608 43.008a43.456 43.456 0 0 1-44.608 43.008zM737.728 512H435.904c-23.68 0-44.672-20.224-44.672-43.008 0-22.784 20.992-43.008 44.608-43.008h299.264c23.616 0 44.608 20.224 44.608 43.008a42.752 42.752 0 0 1-41.984 43.008z m0-202.496H435.904c-23.616 0-44.608-20.224-44.608-43.008 0-22.784 20.992-43.008 44.608-43.008h299.264c23.616 0 44.608 20.224 44.608 43.008a42.88 42.88 0 0 1-42.048 43.008z" p-id="22539" fill="currentColor"></path></svg>`
export const rebootIcon = `<svg t="1714568501931" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4275" width="200" height="200"><path d="M511.721751 0.000278a511.999861 511.999861 0 1 0 512.277971 511.721751A511.721751 511.721751 0 0 0 511.721751 0.000278zM184.386696 511.722029A36.988583 36.988583 0 0 1 222.487718 475.011556h92.888622a36.710473 36.710473 0 0 1 0 73.420947H222.487718a36.710473 36.710473 0 0 1-38.101022-36.710474z m201.351385 158.522499l-65.911986 65.911987a36.988583 36.988583 0 0 1-62.852781-25.864197 38.101022 38.101022 0 0 1 10.846276-26.142307L333.731577 618.238024a36.710473 36.710473 0 1 1 52.006504 52.006504z m29.201513-256.138985a36.710473 36.710473 0 0 1-52.006504 0l-65.633877-65.633877a36.988583 36.988583 0 0 1 26.142307-62.85278 36.154254 36.154254 0 0 1 25.864197 10.846276L414.939594 361.54282a36.988583 36.988583 0 0 1 0 52.562723z m135.439398 373.779366a37.266693 37.266693 0 0 1-36.988583 36.988583 36.988583 36.988583 0 0 1-36.710473-36.988583V695.274397a36.988583 36.988583 0 0 1 36.710473-36.988583A37.266693 37.266693 0 0 1 550.378992 695.274397z m0-459.437137a37.266693 37.266693 0 0 1-36.988583 36.988583 36.988583 36.988583 0 0 1-36.710473-36.988583V235.559149a36.988583 36.988583 0 0 1 36.710473-36.988583 37.544802 37.544802 0 0 1 36.988583 36.988583z m63.965219 15.85225L679.978088 278.109926a36.710473 36.710473 0 0 1 52.006504 51.728394L667.463154 396.584635a37.544802 37.544802 0 0 1-52.284614 0 36.988583 36.988583 0 0 1-10.568166-26.142306 36.432364 36.432364 0 0 1 9.733837-26.142307z m122.090135 397.974905a37.544802 37.544802 0 0 1-52.284613 0l-65.355767-65.911986a36.154254 36.154254 0 0 1 0-51.728395 36.710473 36.710473 0 0 1 25.864197-10.846276 35.876145 35.876145 0 0 1 25.864197 10.846276l65.911986 65.633877a36.988583 36.988583 0 0 1 0 52.006504z m66.468206-194.676753h-92.888622a36.710473 36.710473 0 0 1 0-73.420947h92.888622a36.710473 36.710473 0 0 1 0 73.420947z" fill="currentColor" p-id="4276"></path></svg>`
export const closeIcon = `<svg t="1714965640187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4264" width="200" height="200"><path d="M597.795527 511.488347 813.564755 295.718095c23.833825-23.833825 23.833825-62.47489 0.001023-86.307691-23.832801-23.832801-62.47489-23.833825-86.307691 0L511.487835 425.180656 295.717583 209.410404c-23.833825-23.833825-62.475913-23.833825-86.307691 0-23.832801 23.832801-23.833825 62.47489 0 86.308715l215.769228 215.769228L209.410915 727.258599c-23.833825 23.833825-23.833825 62.47489 0 86.307691 23.832801 23.833825 62.473867 23.833825 86.307691 0l215.768205-215.768205 215.769228 215.769228c23.834848 23.833825 62.475913 23.832801 86.308715 0 23.833825-23.833825 23.833825-62.47489 0-86.307691L597.795527 511.488347z" fill="currentColor" p-id="4265"></path></svg>`

View File

@@ -0,0 +1,683 @@
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { api } from "../../../../scripts/api.js";
import {formatTime} from './utils.js';
import {$t} from "./i18n.js";
import {toast} from "./toast.js";
class MetadataDialog extends ComfyDialog {
constructor() {
super();
this.element.classList.add("easyuse-model-metadata");
}
show(metadata) {
super.show(
$el(
"div",
Object.keys(metadata).map((k) =>
$el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })])
)
)
);
}
}
export class ModelInfoDialog extends ComfyDialog {
constructor(name) {
super();
this.name = name;
this.element.classList.add("easyuse-model-info");
}
get customNotes() {
return this.metadata["easyuse.notes"];
}
set customNotes(v) {
this.metadata["easyuse.notes"] = v;
}
get hash() {
return this.metadata["easyuse.sha256"];
}
async show(type, value) {
this.type = type;
const req = api.fetchApi("/easyuse/metadata/" + encodeURIComponent(`${type}/${value}`));
this.info = $el("div", { style: { flex: "auto" } });
// this.img = $el("img", { style: { display: "none" } });
this.imgCurrent = 0
this.imgList = $el("div.easyuse-preview-list",{
style: { display: "none" }
})
this.imgWrapper = $el("div.easyuse-preview", [
$el("div.easyuse-preview-group",[
this.imgList
]),
]);
this.main = $el("main", { style: { display: "flex" } }, [this.imgWrapper, this.info]);
this.content = $el("div.easyuse-model-content", [
$el("div.easyuse-model-header",[$el("h2", { textContent: this.name })])
, this.main]);
const loading = $el("div", { textContent: " Loading...", parent: this.content });
super.show(this.content);
this.metadata = await (await req).json();
this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = "";
this.viewMetadata.removeAttribute("disabled");
loading.remove();
this.addInfo();
}
createButtons() {
const btns = super.createButtons();
this.viewMetadata = $el("button", {
type: "button",
textContent: "View raw metadata",
disabled: "disabled",
style: {
opacity: 0.5,
cursor: "not-allowed",
},
onclick: (e) => {
if (this.metadata) {
new MetadataDialog().show(this.metadata);
}
},
});
btns.unshift(this.viewMetadata);
return btns;
}
parseNote() {
if (!this.customNotes) return [];
let notes = [];
// Extract links from notes
const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
let end = 0;
let m;
do {
m = r.exec(this.customNotes);
let pos;
let fin = 0;
if (m) {
pos = m.index;
fin = m.index + m[0].length;
} else {
pos = this.customNotes.length;
}
let pre = this.customNotes.substring(end, pos);
if (pre) {
pre = pre.replaceAll("\n", "<br>");
notes.push(
$el("span", {
innerHTML: pre,
})
);
}
if (m) {
notes.push(
$el("a", {
href: m[0],
textContent: m[0],
target: "_blank",
})
);
}
end = fin;
} while (m);
return notes;
}
addInfoEntry(name, value) {
return $el(
"p",
{
parent: this.info,
},
[
typeof name === "string" ? $el("label", { textContent: name + ": " }) : name,
typeof value === "string" ? $el("span", { textContent: value }) : value,
]
);
}
async getCivitaiDetails() {
const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash);
if (req.status === 200) {
return await req.json();
} else if (req.status === 404) {
throw new Error("Model not found");
} else {
throw new Error(`Error loading info (${req.status}) ${req.statusText}`);
}
}
addCivitaiInfo() {
const promise = this.getCivitaiDetails();
const content = $el("span", { textContent: " Loading..." });
this.addInfoEntry(
$el("label", [
$el("img", {
style: {
width: "18px",
position: "relative",
top: "3px",
margin: "0 5px 0 0",
},
src: "https://civitai.com/favicon.ico",
}),
$el("span", { textContent: "Civitai: " }),
]),
content
);
return promise
.then((info) => {
this.imgWrapper.style.display = 'block'
// 变更标题信息
let header = this.element.querySelector('.easyuse-model-header')
if(header){
header.replaceChildren(
$el("h2", { textContent: this.name }),
$el("div.easyuse-model-header-remark",[
$el("h5", { textContent: $t("Updated At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}),
$el("h5", { textContent: $t("Created At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}),
])
)
}
// 替换内容
let textarea = null
let notes = this.parseNote.call(this)
let editText = $t("✏️ Edit")
console.log(notes)
let textarea_div = $el("div.easyuse-model-detail-textarea",[
$el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}),
])
if(!notes || notes.length == 0) textarea_div.classList.add('empty')
else textarea_div.classList.remove('empty')
this.info.replaceChildren(
$el("div.easyuse-model-detail",[
$el("div.easyuse-model-detail-head.flex-b",[
$el('span',$t("Notes")),
$el("a", {
textContent: editText,
href: "#",
style: {
fontSize: "12px",
float: "right",
color: "var(--warning-color)",
textDecoration: "none",
},
onclick: async (e) => {
e.preventDefault();
if (textarea) {
if(textarea.value != this.customNotes){
toast.showLoading($t('Saving Notes...'))
this.customNotes = textarea.value;
const resp = await api.fetchApi(
"/easyuse/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
{
method: "POST",
body: this.customNotes,
}
);
toast.hideLoading()
if (resp.status !== 200) {
toast.error($t('Saving Failed'))
console.error(resp);
alert(`Error saving notes (${resp.status}) ${resp.statusText}`);
return;
}
toast.success($t('Saving Succeed'))
notes = this.parseNote.call(this)
console.log(notes)
textarea_div.replaceChildren($el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}));
if(textarea.value) textarea_div.classList.remove('empty')
else textarea_div.classList.add('empty')
}else {
textarea_div.replaceChildren($el("p",{textContent:$t('No notes')}));
textarea_div.classList.add('empty')
}
e.target.textContent = editText;
textarea.remove();
textarea = null;
} else {
e.target.textContent = "💾 Save";
textarea = $el("textarea", {
placeholder: $t("Type your notes here"),
style: {
width: "100%",
minWidth: "200px",
minHeight: "50px",
height:"100px"
},
textContent: this.customNotes,
});
textarea_div.replaceChildren(textarea);
textarea.focus()
}
}
})
]),
textarea_div
]),
$el("div.easyuse-model-detail",[
$el("div.easyuse-model-detail-head",{textContent:$t("Details")}),
$el("div.easyuse-model-detail-body",[
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Type")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.model.type}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("BaseModel")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.baseModel}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Download")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.stats?.downloadCount || 0}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Trained Words")}),
$el("div.easyuse-model-detail-item-value",{textContent:info?.trainedWords.join(',') || '-'}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Source")}),
$el("div.easyuse-model-detail-item-value",[
$el("label", [
$el("img", {
style: {
width: "14px",
position: "relative",
top: "3px",
margin: "0 5px 0 0",
},
src: "https://civitai.com/favicon.ico",
}),
$el("a", {
href: "https://civitai.com/models/" + info.modelId,
textContent: "View " + info.model.name,
target: "_blank",
})
])
]),
])
]),
])
);
if (info.images?.length) {
this.imgCurrent = 0
this.isSaving = false
info.images.map(cate=>
cate.url &&
this.imgList.appendChild(
$el('div.easyuse-preview-slide',[
$el('div.easyuse-preview-slide-content',[
$el('img',{src:(cate.url)}),
$el("div.save", {
textContent: "Save as preview",
onclick: async () => {
if(this.isSaving) return
this.isSaving = true
toast.showLoading($t('Saving Preview...'))
// Convert the preview to a blob
const blob = await (await fetch(cate.url)).blob();
// Store it in temp
const name = "temp_preview." + new URL(cate.url).pathname.split(".")[1];
const body = new FormData();
body.append("image", new File([blob], name));
body.append("overwrite", "true");
body.append("type", "temp");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status !== 200) {
this.isSaving = false
toast.error($t('Saving Failed'))
toast.hideLoading()
console.error(resp);
alert(`Error saving preview (${req.status}) ${req.statusText}`);
return;
}
// Use as preview
await api.fetchApi("/easyuse/save/" + encodeURIComponent(`${this.type}/${this.name}`), {
method: "POST",
body: JSON.stringify({
filename: name,
type: "temp",
}),
headers: {
"content-type": "application/json",
},
}).then(_=>{
toast.success($t('Saving Succeed'))
toast.hideLoading()
});
this.isSaving = false
app.refreshComboInNodes();
},
})
])
])
)
)
let _this = this
this.imgDistance = (-660 * this.imgCurrent).toString()
this.imgList.style.display = ''
this.imgList.style.transform = 'translate3d(' + this.imgDistance +'px, 0px, 0px)'
this.slides = this.imgList.querySelectorAll('.easyuse-preview-slide')
// 添加按钮
this.slideLeftButton = $el("button.left",{
parent: this.imgWrapper,
style:{
display:info.images.length <= 2 ? 'none' : 'block'
},
innerHTML:`<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: rotate(90deg);"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
onclick: ()=>{
if(info.images.length <= 2) return
_this.imgList.classList.remove("no-transition")
if(_this.imgCurrent == 0){
_this.imgCurrent = (info.images.length/2)-1
this.slides[this.slides.length-1].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)'
this.slides[this.slides.length-2].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)'
_this.imgList.style.transform = 'translate3d(660px, 0px, 0px)'
setTimeout(_=>{
this.slides[this.slides.length-1].style.transform = 'translate3d(0px, 0px, 0px)'
this.slides[this.slides.length-2].style.transform = 'translate3d(0px, 0px, 0px)'
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
_this.imgList.classList.add("no-transition")
},500)
}
else {
_this.imgCurrent = _this.imgCurrent-1
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
}
}
})
this.slideRightButton = $el("button.right",{
parent: this.imgWrapper,
style:{
display:info.images.length <= 2 ? 'none' : 'block'
},
innerHTML:`<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: rotate(-90deg);"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
onclick: ()=>{
if(info.images.length <= 2) return
_this.imgList.classList.remove("no-transition")
if( _this.imgCurrent >= (info.images.length/2)-1){
_this.imgCurrent = 0
const max = info.images.length/2
this.slides[0].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)'
this.slides[1].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)'
_this.imgList.style.transform = 'translate3d(' + (-660 * max).toString()+'px, 0px, 0px)'
setTimeout(_=>{
this.slides[0].style.transform = 'translate3d(0px, 0px, 0px)'
this.slides[1].style.transform = 'translate3d(0px, 0px, 0px)'
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
_this.imgList.classList.add("no-transition")
},500)
}
else {
_this.imgCurrent = _this.imgCurrent+1
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
}
}
})
}
if(info.description){
$el("div", {
parent: this.content,
innerHTML: info.description,
style: {
marginTop: "10px",
},
});
}
return info;
})
.catch((err) => {
this.imgWrapper.style.display = 'none'
content.textContent = "⚠️ " + err.message;
})
.finally(_=>{
})
}
}
export class CheckpointInfoDialog extends ModelInfoDialog {
async addInfo() {
// super.addInfo();
await this.addCivitaiInfo();
}
}
const MAX_TAGS = 500
export class LoraInfoDialog extends ModelInfoDialog {
getTagFrequency() {
if (!this.metadata.ss_tag_frequency) return [];
const datasets = JSON.parse(this.metadata.ss_tag_frequency);
const tags = {};
for (const setName in datasets) {
const set = datasets[setName];
for (const t in set) {
if (t in tags) {
tags[t] += set[t];
} else {
tags[t] = set[t];
}
}
}
return Object.entries(tags).sort((a, b) => b[1] - a[1]);
}
getResolutions() {
let res = [];
if (this.metadata.ss_bucket_info) {
const parsed = JSON.parse(this.metadata.ss_bucket_info);
if (parsed?.buckets) {
for (const { resolution, count } of Object.values(parsed.buckets)) {
res.push([count, `${resolution.join("x")} * ${count}`]);
}
}
}
res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]);
let r = this.metadata.ss_resolution;
if (r) {
const s = r.split(",");
const w = s[0].replace("(", "");
const h = s[1].replace(")", "");
res.push(`${w.trim()}x${h.trim()} (Base res)`);
} else if ((r = this.metadata["modelspec.resolution"])) {
res.push(r + " (Base res");
}
if (!res.length) {
res.push("⚠️ Unknown");
}
return res;
}
getTagList(tags) {
return tags.map((t) =>
$el(
"li.easyuse-model-tag",
{
dataset: {
tag: t[0],
},
$: (el) => {
el.onclick = () => {
el.classList.toggle("easyuse-model-tag--selected");
};
},
},
[
$el("p", {
textContent: t[0],
}),
$el("span", {
textContent: t[1],
}),
]
)
);
}
addTags() {
let tags = this.getTagFrequency();
let hasMore;
if (tags?.length) {
const c = tags.length;
let list;
if (c > MAX_TAGS) {
tags = tags.slice(0, MAX_TAGS);
hasMore = $el("p", [
$el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }),
$el("a", {
href: "#",
textContent: `Show all ${c}`,
onclick: () => {
list.replaceChildren(...this.getTagList(this.getTagFrequency()));
hasMore.remove();
},
}),
]);
}
list = $el("ol.easyuse-model-tags-list", this.getTagList(tags));
this.tags = $el("div", [list]);
} else {
this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" });
}
this.content.append(this.tags);
if (hasMore) {
this.content.append(hasMore);
}
}
async addInfo() {
// this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
// this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
// this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
//
// this.addInfoEntry(
// "Resolution",
// $el(
// "select",
// this.getResolutions().map((r) => $el("option", { textContent: r }))
// )
// );
// super.addInfo();
const p = this.addCivitaiInfo();
this.addTags();
const info = await p;
if (info) {
// $el(
// "p",
// {
// parent: this.content,
// textContent: "Trained Words: ",
// },
// [
// $el("pre", {
// textContent: info.trainedWords.join(", "),
// style: {
// whiteSpace: "pre-wrap",
// margin: "10px 0",
// background: "#222",
// padding: "5px",
// borderRadius: "5px",
// maxHeight: "250px",
// overflow: "auto",
// },
// }),
// ]
// );
$el("div", {
parent: this.content,
innerHTML: info.description,
style: {
maxHeight: "250px",
overflow: "auto",
},
});
}
}
createButtons() {
const btns = super.createButtons();
function copyTags(e, tags) {
const textarea = $el("textarea", {
parent: document.body,
style: {
position: "fixed",
},
textContent: tags.map((el) => el.dataset.tag).join(", "),
});
textarea.select();
try {
document.execCommand("copy");
if (!e.target.dataset.text) {
e.target.dataset.text = e.target.textContent;
}
e.target.textContent = "Copied " + tags.length + " tags";
setTimeout(() => {
e.target.textContent = e.target.dataset.text;
}, 1000);
} catch (ex) {
prompt("Copy to clipboard: Ctrl+C, Enter", text);
} finally {
document.body.removeChild(textarea);
}
}
btns.unshift(
$el("button", {
type: "button",
textContent: "Copy Selected",
onclick: (e) => {
copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag--selected")]);
},
}),
$el("button", {
type: "button",
textContent: "Copy All",
onclick: (e) => {
copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag")]);
},
})
);
return btns;
}
}

View File

@@ -0,0 +1,127 @@
import {sleep} from "./utils.js";
import {$t} from "./i18n.js";
class Toast{
constructor() {
this.info_icon = `<svg focusable="false" data-icon="info-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg>`
this.success_icon = `<svg focusable="false" data-icon="check-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path></svg>`
this.error_icon = `<svg focusable="false" data-icon="close-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" fill-rule="evenodd" viewBox="64 64 896 896"><path d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"></path></svg>`
this.warn_icon = `<svg focusable="false" data-icon="exclamation-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg>`
this.loading_icon = `<svg focusable="false" data-icon="loading" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024"><path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path></svg>`
}
async showToast(data){
let container = document.querySelector(".easyuse-toast-container");
if (!container) {
container = document.createElement("div");
container.classList.add("easyuse-toast-container");
document.body.appendChild(container);
}
await this.hideToast(data.id);
const toastContainer = document.createElement("div");
const content = document.createElement("span");
content.innerHTML = data.content;
toastContainer.appendChild(content);
for (let a = 0; a < (data.actions || []).length; a++) {
const action = data.actions[a];
if (a > 0) {
const sep = document.createElement("span");
sep.innerHTML = "&nbsp;|&nbsp;";
toastContainer.appendChild(sep);
}
const actionEl = document.createElement("a");
actionEl.innerText = action.label;
if (action.href) {
actionEl.target = "_blank";
actionEl.href = action.href;
}
if (action.callback) {
actionEl.onclick = (e) => {
return action.callback(e);
};
}
toastContainer.appendChild(actionEl);
}
const animContainer = document.createElement("div");
animContainer.setAttribute("toast-id", data.id);
animContainer.appendChild(toastContainer);
container.appendChild(animContainer);
await sleep(64);
animContainer.style.marginTop = `-${animContainer.offsetHeight}px`;
await sleep(64);
animContainer.classList.add("-show");
if (data.duration) {
await sleep(data.duration);
this.hideToast(data.id);
}
}
async hideToast(id) {
const msg = document.querySelector(`.easyuse-toast-container > [toast-id="${id}"]`);
if (msg === null || msg === void 0 ? void 0 : msg.classList.contains("-show")) {
msg.classList.remove("-show");
await sleep(750);
}
msg && msg.remove();
}
async clearAllMessages() {
let container = document.querySelector(".easyuse-toast-container");
container && (container.innerHTML = "");
}
async copyright(duration = 5000, actions = []) {
this.showToast({
id: `toast-info`,
content: `${this.info_icon} ${$t('Workflow created by')} <a href="https://github.com/yolain/">Yolain</a> , ${$t('Watch more video content')} <a href="https://space.bilibili.com/1840885116">B站乱乱呀</a>`,
duration,
actions
});
}
async info(content, duration = 3000, actions = []) {
this.showToast({
id: `toast-info`,
content: `${this.info_icon} ${content}`,
duration,
actions
});
}
async success(content, duration = 3000, actions = []) {
this.showToast({
id: `toast-success`,
content: `${this.success_icon} ${content}`,
duration,
actions
});
}
async error(content, duration = 3000, actions = []) {
this.showToast({
id: `toast-error`,
content: `${this.error_icon} ${content}`,
duration,
actions
});
}
async warn(content, duration = 3000, actions = []) {
this.showToast({
id: `toast-warn`,
content: `${this.warn_icon} ${content}`,
duration,
actions
});
}
async showLoading(content, duration = 0, actions = []) {
this.showToast({
id: `toast-loading`,
content: `${this.loading_icon} ${content}`,
duration,
actions
});
}
async hideLoading() {
this.hideToast("toast-loading");
}
}
export const toast = new Toast();

View File

@@ -0,0 +1,187 @@
export function sleep(ms = 100, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, ms);
});
}
export function addPreconnect(href, crossorigin=false){
const preconnect = document.createElement("link");
preconnect.rel = 'preconnect'
preconnect.href = href
if(crossorigin) preconnect.crossorigin = ''
document.head.appendChild(preconnect);
}
export function addCss(href, base=true) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = base ? "extensions/ComfyUI-Easy-Use/"+href : href;
document.head.appendChild(link);
}
export function addMeta(name, content) {
const meta = document.createElement("meta");
meta.setAttribute("name", name);
meta.setAttribute('content', content);
document.head.appendChild(meta);
}
export function deepEqual(obj1, obj2) {
if (typeof obj1 !== typeof obj2) {
return false
}
if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
return obj1 === obj2
}
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (let key of keys1) {
if (!deepEqual(obj1[key], obj2[key])) {
return false
}
}
return true
}
export function getLocale(){
const locale = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US'
return locale
}
export function spliceExtension(fileName){
return fileName.substring(0,fileName.lastIndexOf('.'))
}
export function getExtension(fileName){
return fileName.substring(fileName.lastIndexOf('.') + 1)
}
export function formatTime(time, format) {
time = typeof (time) === "number" ? time : (time instanceof Date ? time.getTime() : parseInt(time));
if (isNaN(time)) return null;
if (typeof (format) !== 'string' || !format) format = 'yyyy-MM-dd hh:mm:ss';
let _time = new Date(time);
time = _time.toString().split(/[\s\:]/g).slice(0, -2);
time[1] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'][_time.getMonth()];
let _mapping = {
MM: 1,
dd: 2,
yyyy: 3,
hh: 4,
mm: 5,
ss: 6
};
return format.replace(/([Mmdhs]|y{2})\1/g, (key) => time[_mapping[key]]);
}
let origProps = {};
export const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name);
export const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false;
export function updateNodeHeight(node) {node.setSize([node.size[0], node.computeSize()[1]]);}
export function toggleWidget(node, widget, show = false, suffix = "") {
if (!widget || doesInputWithNameExist(node, widget.name)) return;
if (!origProps[widget.name]) {
origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
}
const origSize = node.size;
widget.type = show ? origProps[widget.name].origType : "easyHidden" + suffix;
widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show));
const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1];
node.setSize([node.size[0], height]);
}
export function isLocalNetwork(ip) {
const localNetworkRanges = [
'192.168.',
'10.',
'127.',
/^172\.((1[6-9]|2[0-9]|3[0-1])\.)/
];
return localNetworkRanges.some(range => {
if (typeof range === 'string') {
return ip.startsWith(range);
} else {
return range.test(ip);
}
});
}
/**
* accAdd 高精度加法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accAdd(arg1, arg2) {
let r1, r2, s1, s2,max;
s1 = typeof arg1 == 'string' ? arg1 : arg1.toString()
s2 = typeof arg2 == 'string' ? arg2 : arg2.toString()
try { r1 = s1.split(".")[1].length } catch (e) { r1 = 0 }
try { r2 = s2.split(".")[1].length } catch (e) { r2 = 0 }
max = Math.pow(10, Math.max(r1, r2))
return (arg1 * max + arg2 * max) / max
}
/**
* accSub 高精度减法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accSub(arg1, arg2) {
let r1, r2, max, min,s1,s2;
s1 = typeof arg1 == 'string' ? arg1 : arg1.toString()
s2 = typeof arg2 == 'string' ? arg2 : arg2.toString()
try { r1 = s1.split(".")[1].length } catch (e) { r1 = 0 }
try { r2 = s2.split(".")[1].length } catch (e) { r2 = 0 }
max = Math.pow(10, Math.max(r1, r2));
//动态控制精度长度
min = (r1 >= r2) ? r1 : r2;
return ((arg1 * max - arg2 * max) / max).toFixed(min)
}
/**
* accMul 高精度乘法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accMul(arg1, arg2) {
let max = 0, s1 = typeof arg1 == 'string' ? arg1 : arg1.toString(), s2 = typeof arg2 == 'string' ? arg2 : arg2.toString();
try { max += s1.split(".")[1].length } catch (e) { }
try { max += s2.split(".")[1].length } catch (e) { }
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, max)
}
/**
* accDiv 高精度除法
* @since 1.0.10
* @param {Number} arg1
* @param {Number} arg2
* @return {Number}
*/
export function accDiv(arg1, arg2) {
let t1 = 0, t2 = 0, r1, r2,s1 = typeof arg1 == 'string' ? arg1 : arg1.toString(), s2 = typeof arg2 == 'string' ? arg2 : arg2.toString();
try { t1 = s1.toString().split(".")[1].length } catch (e) { }
try { t2 = s2.toString().split(".")[1].length } catch (e) { }
r1 = Number(s1.toString().replace(".", ""))
r2 = Number(s2.toString().replace(".", ""))
return (r1 / r2) * Math.pow(10, t2 - t1)
}
Number.prototype.div = function (arg) {
return accDiv(this, arg);
}

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);
}
}
});

View File

@@ -0,0 +1,311 @@
import { app } from "../../../scripts/app.js";
// Node that allows you to tunnel connections for cleaner graphs
app.registerExtension({
name: "easy setNode",
registerCustomNodes() {
class SetNode {
defaultVisibility = true;
serialize_widgets = true;
constructor() {
if (!this.properties) {
this.properties = {
"previousName": ""
};
}
this.properties.showOutputText = SetNode.defaultVisibility;
const node = this;
node.color = LGraphCanvas.node_colors.blue.color;
this.addWidget(
"text",
"Constant",
'',
(s, t, u, v, x) => {
node.validateName(node.graph);
if(this.widgets[0].value !== ''){
this.title = "Set_" + this.widgets[0].value;
}
this.update();
this.properties.previousName = this.widgets[0].value;
},
{}
)
this.addInput("*", "*");
this.onConnectionsChange = function(
slotType, //1 = input, 2 = output
slot,
isChangeConnect,
link_info,
output
) {
// console.log("onConnectionsChange");
//On Disconnect
if (slotType == 1 && !isChangeConnect) {
this.inputs[slot].type = '*';
this.inputs[slot].name = '*';
}
//On Connect
if (link_info && node.graph && slotType == 1 && isChangeConnect) {
const fromNode = node.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id);
const type = fromNode.outputs[link_info.origin_slot].type;
if (this.title === "Set"){
this.title = "Set_" + type;
}
if (this.widgets[0].value === '*'){
this.widgets[0].value = type
}
this.validateName(node.graph);
this.inputs[0].type = type;
this.inputs[0].name = type;
setTimeout(_=>{
if(type != this.widgets[0].value){
this.title = "Set_" + this.widgets[0].value;
}
},1)
}
//Update either way
this.update();
}
this.validateName = function(graph) {
let widgetValue = node.widgets[0].value;
if (widgetValue != '') {
let tries = 0;
let collisions = [];
do {
collisions = graph._nodes.filter((otherNode) => {
if (otherNode == this) {
return false;
}
if (otherNode.type == 'easy setNode' && otherNode.widgets[0].value === widgetValue) {
return true;
}
return false;
})
if (collisions.length > 0) {
widgetValue = node.widgets[0].value + "_" + tries;
}
tries++;
} while (collisions.length > 0)
node.widgets[0].value = widgetValue;
this.update();
}
}
this.clone = function () {
const cloned = SetNode.prototype.clone.apply(this);
cloned.inputs[0].name = '*';
cloned.inputs[0].type = '*';
cloned.properties.previousName = '';
cloned.size = cloned.computeSize();
return cloned;
};
this.onAdded = function(graph) {
this.validateName(graph);
}
this.update = function() {
if (node.graph) {
this.findGetters(node.graph).forEach((getter) => {
getter.setType(this.inputs[0].type);
});
if (this.widgets[0].value) {
this.findGetters(node.graph, true).forEach((getter) => {
getter.setName(this.widgets[0].value)
});
}
const allGetters = node.graph._nodes.filter((otherNode) => otherNode.type == "easy getNode");
allGetters.forEach((otherNode) => {
if (otherNode.setComboValues) {
otherNode.setComboValues();
}
})
}
}
this.findGetters = function(graph, checkForPreviousName) {
const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value;
return graph._nodes.filter((otherNode) => {
if (otherNode.type == 'easy getNode' && otherNode.widgets[0].value === name && name != '') {
return true;
}
return false;
})
}
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
this.isVirtualNode = true;
}
onRemoved() {
const allGetters = this.graph._nodes.filter((otherNode) => otherNode.type == "easy getNode");
allGetters.forEach((otherNode) => {
if (otherNode.setComboValues) {
otherNode.setComboValues([this]);
}
})
}
}
LiteGraph.registerNodeType(
"easy setNode",
Object.assign(SetNode, {
title: "Set",
})
);
SetNode.category = "EasyUse/Util";
},
});
app.registerExtension({
name: "easy getNode",
registerCustomNodes() {
class GetNode {
defaultVisibility = true;
serialize_widgets = true;
constructor() {
if (!this.properties) {
this.properties = {};
}
this.properties.showOutputText = GetNode.defaultVisibility;
const node = this;
node.color = LGraphCanvas.node_colors.blue.color;
this.addWidget(
"combo",
"Constant",
"",
(e) => {
this.onRename();
},
{
values: () => {
const setterNodes = node.graph._nodes.filter((otherNode) => otherNode.type == 'easy setNode');
return setterNodes.map((otherNode) => otherNode.widgets[0].value).sort();
}
}
)
this.addOutput("*", '*');
this.onConnectionsChange = function(
slotType, //0 = output, 1 = input
slot, //self-explanatory
isChangeConnect,
link_info,
output
) {
this.validateLinks();
setTimeout(_=>{
this.title = 'Get_' + this.widgets[0].value
},1)
}
this.setName = function(name) {
node.widgets[0].value = name;
node.onRename();
node.serialize();
}
this.onRename = function() {
const setter = this.findSetter(node.graph);
if (setter) {
this.setType(setter.inputs[0].type);
this.title = "Get_" + setter.widgets[0].value;
} else {
this.setType('*');
}
}
this.clone = function () {
const cloned = GetNode.prototype.clone.apply(this);
cloned.size = cloned.computeSize();
return cloned;
};
this.validateLinks = function() {
if (this.outputs[0].type != '*' && this.outputs[0].links) {
this.outputs[0].links.forEach((linkId) => {
const link = node.graph.links[linkId];
if (link && link.type != this.outputs[0].type && link.type != '*') {
node.graph.removeLink(linkId)
}
})
}
}
this.setType = function(type) {
this.outputs[0].name = type;
this.outputs[0].type = type;
this.validateLinks();
}
this.findSetter = function(graph) {
const name = this.widgets[0].value;
return graph._nodes.find((otherNode) => {
if (otherNode.type == 'easy setNode' && otherNode.widgets[0].value === name && name != '') {
return true;
}
return false;
})
}
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
this.isVirtualNode = true;
}
getInputLink(slot) {
const setter = this.findSetter(this.graph);
if (setter) {
const slot_info = setter.inputs[slot];
const link = this.graph.links[ slot_info.link ];
return link;
} else {
throw new Error("No setter found for " + this.widgets[0].value + "(" + this.type + ")");
}
}
onAdded(graph) {
//this.setComboValues();
//this.validateName(graph);
}
}
LiteGraph.registerNodeType(
"easy getNode",
Object.assign(GetNode, {
title: "Get",
})
);
GetNode.category = "EasyUse/Util";
},
});

View File

@@ -0,0 +1,66 @@
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "comfy.easyUse.imageWidgets",
nodeCreated(node) {
if (["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide","easy imageSizeShow", "easy imageRatio", "easy imagePixelPerfect"].includes(node.comfyClass)) {
const inputEl = document.createElement("textarea");
inputEl.className = "comfy-multiline-input";
inputEl.readOnly = true
const widget = node.addDOMWidget("info", "customtext", inputEl, {
getValue() {
return inputEl.value;
},
setValue(v) {
inputEl.value = v;
},
serialize: false
});
widget.inputEl = inputEl;
inputEl.addEventListener("input", () => {
widget.callback?.(widget.value);
});
}
},
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide", "easy imageSizeShow", "easy imageRatio", "easy imagePixelPerfect"].includes(nodeData.name)) {
function populate(arr_text) {
var text = '';
for (let i = 0; i < arr_text.length; i++){
text += arr_text[i];
}
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === "info");
if (pos !== -1 && this.widgets[pos]) {
const w = this.widgets[pos]
w.value = text;
}
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
}
}
})

View File

@@ -0,0 +1,237 @@
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { ComfyDialog, $el } from "../../../../scripts/ui.js";
import { restart_from_here } from "./prompt.js";
import { hud, FlowState } from "./state.js";
import { send_cancel, send_message, send_onstart, skip_next_restart_message } from "./messaging.js";
import { display_preview_images, additionalDrawBackground, click_is_in_image } from "./preview.js";
import {$t} from "../common/i18n.js";
class chooserImageDialog extends ComfyDialog {
constructor() {
super();
this.node = null
this.select_index = []
this.dialog_div = null
}
show(image,node){
this.select_index = []
this.node = node
const images_div = image.map((img, index) => {
const imgEl = $el('img', {
src: img.src,
onclick: _ => {
if(this.select_index.includes(index)){
this.select_index = this.select_index.filter(i => i !== index)
imgEl.classList.remove('selected')
} else {
this.select_index.push(index)
imgEl.classList.add('selected')
}
if (node.selected.has(index)) node.selected.delete(index);
else node.selected.add(index);
}
})
return imgEl
})
super.show($el('div.easyuse-chooser-dialog',[
$el('h5.easyuse-chooser-dialog-title', $t('Choose images to continue')),
$el('div.easyuse-chooser-dialog-images',images_div)
]))
}
createButtons() {
const btns = super.createButtons();
btns[0].onclick = _ => {
if (FlowState.running()) { send_cancel();}
super.close()
}
btns.unshift($el('button', {
type: 'button',
textContent: $t('Choose Selected Images'),
onclick: _ => {
if (FlowState.paused()) {
send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]);
}
if (FlowState.idle()) {
skip_next_restart_message();
restart_from_here(this.node.id).then(() => { send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]); });
}
super.close()
}
}))
return btns
}
}
function progressButtonPressed() {
const node = app.graph._nodes_by_id[this.node_id];
if (node) {
const selected = [...node.selected]
if(selected?.length>0){
node.setProperty('values',selected)
}
if (FlowState.paused()) {
send_message(node.id, [...node.selected, -1, ...node.anti_selected]);
}
if (FlowState.idle()) {
skip_next_restart_message();
restart_from_here(node.id).then(() => { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); });
}
}
}
function cancelButtonPressed() {
if (FlowState.running()) { send_cancel();}
}
function enable_disabling(button) {
Object.defineProperty(button, 'clicked', {
get : function() { return this._clicked; },
set : function(v) { this._clicked = (v && this.name!=''); }
})
}
function disable_serialize(widget) {
if (!widget.options) widget.options = { };
widget.options.serialize = false;
}
app.registerExtension({
name:'comfy.easyuse.imageChooser',
init() {
window.addEventListener("beforeunload", send_cancel, true);
},
setup(app) {
const draw = LGraphCanvas.prototype.draw;
LGraphCanvas.prototype.draw = function() {
if (hud.update()) {
app.graph._nodes.forEach((node)=> { if (node.update) { node.update(); } })
}
draw.apply(this,arguments);
}
function easyuseImageChooser(event) {
const {node,image,isKSampler} = display_preview_images(event);
if(isKSampler) {
const dialog = new chooserImageDialog();
dialog.show(image,node)
}
}
api.addEventListener("easyuse-image-choose", easyuseImageChooser);
/*
If a run is interrupted, send a cancel message (unless we're doing the cancelling, to avoid infinite loop)
*/
const original_api_interrupt = api.interrupt;
api.interrupt = function () {
if (FlowState.paused() && !FlowState.cancelling) send_cancel();
original_api_interrupt.apply(this, arguments);
}
/*
At the start of execution
*/
function on_execution_start() {
if (send_onstart()) {
app.graph._nodes.forEach((node)=> {
if (node.selected || node.anti_selected) {
node.selected.clear();
node.anti_selected.clear();
node.update();
}
})
}
}
api.addEventListener("execution_start", on_execution_start);
},
async nodeCreated(node, app) {
if(node.comfyClass == 'easy imageChooser'){
node.setProperty('values',[])
/* A property defining the top of the image when there is just one */
if(node?.imageIndex === undefined){
Object.defineProperty(node, 'imageIndex', {
get : function() { return null; },
set: function (v) {node.overIndex= v},
})
}
if(node?.imagey === undefined){
Object.defineProperty(node, 'imagey', {
get : function() { return null; },
set: function (v) {return node.widgets[node.widgets.length-1].last_y+LiteGraph.NODE_WIDGET_HEIGHT;},
})
}
/* Capture clicks */
const org_onMouseDown = node.onMouseDown;
node.onMouseDown = function( e, pos, canvas ) {
if (e.isPrimary) {
const i = click_is_in_image(node, pos);
if (i>=0) { this.imageClicked(i); }
}
return (org_onMouseDown && org_onMouseDown.apply(this, arguments));
}
node.send_button_widget = node.addWidget("button", "", "", progressButtonPressed);
node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed);
enable_disabling(node.cancel_button_widget);
enable_disabling(node.send_button_widget);
disable_serialize(node.cancel_button_widget);
disable_serialize(node.send_button_widget);
}
},
beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData?.name == 'easy imageChooser'){
const onDrawBackground = nodeType.prototype.onDrawBackground;
nodeType.prototype.onDrawBackground = function(ctx) {
onDrawBackground.apply(this, arguments);
additionalDrawBackground(this, ctx);
}
nodeType.prototype.imageClicked = function (imageIndex) {
if (nodeType?.comfyClass==="easy imageChooser") {
if (this.selected.has(imageIndex)) this.selected.delete(imageIndex);
else this.selected.add(imageIndex);
this.update();
}
}
const update = nodeType.prototype.update;
nodeType.prototype.update = function() {
if (update) update.apply(this,arguments);
if (this.send_button_widget) {
this.send_button_widget.node_id = this.id;
const selection = ( this.selected ? this.selected.size : 0 ) + ( this.anti_selected ? this.anti_selected.size : 0 )
const maxlength = this.imgs?.length || 0;
if (FlowState.paused_here(this.id) && selection>0) {
this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image";
} else if (selection>0) {
this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image as restart";
}
else {
this.send_button_widget.name = "";
}
}
if (this.cancel_button_widget) {
const isRunning = FlowState.running()
this.cancel_button_widget.name = isRunning ? "Cancel current run" : "";
}
this.setDirtyCanvas(true,true);
}
}
}
})

View File

@@ -0,0 +1,34 @@
import { api } from "../../../../scripts/api.js";
import { FlowState } from "./state.js";
function send_message_from_pausing_node(message) {
const id = app.runningNodeId;
send_message(id, message);
}
function send_message(id, message) {
const body = new FormData();
body.append('message',message);
body.append('id', id);
api.fetchApi("/easyuse/image_chooser_message", { method: "POST", body, });
}
function send_cancel() {
send_message(-1,'__cancel__');
FlowState.cancelling = true;
api.interrupt();
FlowState.cancelling = false;
}
var skip_next = 0;
function skip_next_restart_message() { skip_next += 1; }
function send_onstart() {
if (skip_next>0) {
skip_next -= 1;
return false;
}
send_message(-1,'__start__');
return true;
}
export { send_message_from_pausing_node, send_cancel, send_message, send_onstart, skip_next_restart_message }

View File

@@ -0,0 +1,90 @@
import { app } from "../../../../scripts/app.js";
const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy fullkSampler']
function display_preview_images(event) {
const node = app.graph._nodes_by_id[event.detail.id];
if (node) {
node.selected = new Set();
node.anti_selected = new Set();
const image = showImages(node, event.detail.urls);
return {node,image,isKSampler:kSampler.includes(node.type)}
} else {
console.log(`Image Chooser Preview - failed to find ${event.detail.id}`)
}
}
function showImages(node, urls) {
node.imgs = [];
urls.forEach((u)=> {
const img = new Image();
node.imgs.push(img);
img.onload = () => { app.graph.setDirtyCanvas(true); };
img.src = `/view?filename=${encodeURIComponent(u.filename)}&type=temp&subfolder=${app.getPreviewFormatParam()}`
})
node.setSizeForImage?.();
return node.imgs
}
function drawRect(node, s, ctx) {
const padding = 1;
var rect;
if (node.imageRects) {
rect = node.imageRects[s];
} else {
const y = node.imagey;
rect = [padding,y+padding,node.size[0]-2*padding,node.size[1]-y-2*padding];
}
ctx.strokeRect(rect[0]+padding, rect[1]+padding, rect[2]-padding*2, rect[3]-padding*2);
}
function additionalDrawBackground(node, ctx) {
if (!node.imgs) return;
if (node.imageRects) {
for (let i = 0; i < node.imgs.length; i++) {
// delete underlying image
ctx.fillStyle = "#000";
ctx.fillRect(...node.imageRects[i])
// draw the new one
const img = node.imgs[i];
const cellWidth = node.imageRects[i][2];
const cellHeight = node.imageRects[i][3];
let wratio = cellWidth/img.width;
let hratio = cellHeight/img.height;
var ratio = Math.min(wratio, hratio);
let imgHeight = ratio * img.height;
let imgWidth = ratio * img.width;
const imgX = node.imageRects[i][0] + (cellWidth - imgWidth)/2;
const imgY = node.imageRects[i][1] + (cellHeight - imgHeight)/2;
const cell_padding = 2;
ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2);
}
}
ctx.lineWidth = 2;
ctx.strokeStyle = "green";
node?.selected?.forEach((s) => { drawRect(node,s, ctx) })
ctx.strokeStyle = "#F88";
node?.anti_selected?.forEach((s) => { drawRect(node,s, ctx) })
}
function click_is_in_image(node, pos) {
if (node.imgs?.length>1) {
for (var i = 0; i<node.imageRects.length; i++) {
const dx = pos[0] - node.imageRects[i][0];
const dy = pos[1] - node.imageRects[i][1];
if ( dx > 0 && dx < node.imageRects[i][2] &&
dy > 0 && dy < node.imageRects[i][3] ) {
return i;
}
}
} else if (node.imgs?.length==1) {
if (pos[1]>node.imagey) return 0;
}
return -1;
}
export { display_preview_images, additionalDrawBackground, click_is_in_image }

View File

@@ -0,0 +1,114 @@
import { app } from "../../../../scripts/app.js";
function links_with(p, node_id, down, up) {
const links_with = [];
p.workflow.links.forEach((l) => {
if (down && l[1]===node_id && !links_with.includes(l[3])) links_with.push(l[3])
if (up && l[3]===node_id && !links_with.includes(l[1])) links_with.push(l[1])
});
return links_with;
}
function _all_v_nodes(p, here_id) {
/*
Make a list of all downstream nodes.
*/
const downstream = [];
const to_process = [here_id]
while(to_process.length>0) {
const id = to_process.pop();
downstream.push(id);
to_process.push(
...links_with(p,id,true,false).filter((nid)=>{
return !(downstream.includes(nid) || to_process.includes(nid))
})
)
}
/*
Now all upstream nodes from any of the downstream nodes (except us).
Put us on the result list so we don't flow up through us
*/
to_process.push(...downstream.filter((n)=>{ return n!=here_id}));
const back_upstream = [here_id];
while(to_process.length>0) {
const id = to_process.pop();
back_upstream.push(id);
to_process.push(
...links_with(p,id,false,true).filter((nid)=>{
return !(back_upstream.includes(nid) || to_process.includes(nid))
})
)
}
const keep = [];
keep.push(...downstream);
keep.push(...back_upstream.filter((n)=>{return !keep.includes(n)}));
console.log(`Nodes to keep: ${keep}`);
return keep;
}
async function all_v_nodes(here_id) {
const p = structuredClone(await app.graphToPrompt());
const all_nodes = [];
p.workflow.nodes.forEach((node)=>{all_nodes.push(node.id)})
p.workflow.links = p.workflow.links.filter((l)=>{ return (all_nodes.includes(l[1]) && all_nodes.includes(l[3]))} )
return _all_v_nodes(p,here_id);
}
async function restart_from_here(here_id, go_down_to_chooser=false) {
const p = structuredClone(await app.graphToPrompt());
/*
Make a list of all nodes, and filter out links that are no longer valid
*/
const all_nodes = [];
p.workflow.nodes.forEach((node)=>{all_nodes.push(node.id)})
p.workflow.links = p.workflow.links.filter((l)=>{ return (all_nodes.includes(l[1]) && all_nodes.includes(l[3]))} )
/* Move downstream to a chooser */
if (go_down_to_chooser) {
while (!app.graph._nodes_by_id[here_id].isChooser) {
here_id = links_with(p, here_id, true, false)[0];
}
}
const keep = _all_v_nodes(p, here_id);
/*
Filter p.workflow.nodes and p.workflow.links
*/
p.workflow.nodes = p.workflow.nodes.filter((node) => {
if (node.id===here_id) node.inputs.forEach((i)=>{i.link=null}) // remove our upstream links
return (keep.includes(node.id)) // only keep keepers
})
p.workflow.links = p.workflow.links.filter((l) => {return (keep.includes(l[1]) && keep.includes(l[3]))})
/*
Filter the p.output object to only include nodes we're keeping
*/
const new_output = {}
for (const [key, value] of Object.entries(p.output)) {
if (keep.includes(parseInt(key))) new_output[key] = value;
}
/*
Filter the p.output entry for the start node to remove any list (ie link) inputs
*/
const new_inputs = {};
for (const [key, value] of Object.entries(new_output[here_id.toString()].inputs)) {
if (!Array.isArray(value)) new_inputs[key] = value;
}
new_output[here_id.toString()].inputs = new_inputs;
p.output = new_output;
// temporarily hijack graph_to_prompt with a version that restores the old one but returns this prompt
const gtp_was = app.graphToPrompt;
app.graphToPrompt = () => {
app.graphToPrompt = gtp_was;
return p;
}
app.queuePrompt(0);
}
export { restart_from_here, all_v_nodes }

View File

@@ -0,0 +1,55 @@
import { app } from "../../../../scripts/app.js";
class HUD {
constructor() {
this.current_node_id = undefined;
this.class_of_current_node = null;
this.current_node_is_chooser = false;
}
update() {
if (app.runningNodeId==this.current_node_id) return false;
this.current_node_id = app.runningNodeId;
if (this.current_node_id) {
this.class_of_current_node = app.graph?._nodes_by_id[app.runningNodeId.toString()]?.comfyClass;
this.current_node_is_chooser = this.class_of_current_node === "easy imageChooser"
} else {
this.class_of_current_node = undefined;
this.current_node_is_chooser = false;
}
return true;
}
}
const hud = new HUD();
class FlowState {
constructor(){}
static idle() {
return (!app.runningNodeId);
}
static paused() {
return true;
}
static paused_here(node_id) {
return (FlowState.paused() && FlowState.here(node_id))
}
static running() {
return (!FlowState.idle());
}
static here(node_id) {
return (app.runningNodeId==node_id);
}
static state() {
if (FlowState.paused()) return "Paused";
if (FlowState.running()) return "Running";
return "Idle";
}
static cancelling = false;
}
export { hud, FlowState}

View File

@@ -0,0 +1,666 @@
import { app } from "../../../scripts/app.js";
import { fabric } from "../lib/fabric.js";
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerColor = "#108ce6";
fabric.Object.prototype.borderColor = "#108ce6";
fabric.Object.prototype.cornerSize = 10;
let connect_keypoints = [
[0, 1],
[1, 2],
[2, 3],
[3, 4],
[1, 5],
[5, 6],
[6, 7],
[1, 8],
[8, 9],
[9, 10],
[1, 11],
[11, 12],
[12, 13],
[0, 14],
[14, 16],
[0, 15],
[15, 17],
];
let connect_color = [
[0, 0, 255],
[255, 0, 0],
[255, 170, 0],
[255, 255, 0],
[255, 85, 0],
[170, 255, 0],
[85, 255, 0],
[0, 255, 0],
[0, 255, 85],
[0, 255, 170],
[0, 255, 255],
[0, 170, 255],
[0, 85, 255],
[85, 0, 255],
[170, 0, 255],
[255, 0, 255],
[255, 0, 170],
[255, 0, 85],
];
const default_keypoints = [
[241, 77],
[241, 120],
[191, 118],
[177, 183],
[163, 252],
[298, 118],
[317, 182],
[332, 245],
[225, 241],
[213, 359],
[215, 454],
[270, 240],
[282, 360],
[286, 456],
[232, 59],
[253, 60],
[225, 70],
[260, 72],
];
class OpenPose {
constructor(node, canvasElement) {
this.lockMode = false;
this.visibleEyes = true;
this.flipped = false;
this.node = node;
this.undo_history = LS_Poses[node.name].undo_history || [];
this.redo_history = LS_Poses[node.name].redo_history || [];
this.history_change = false;
this.canvas = this.initCanvas(canvasElement);
this.image = node.widgets.find((w) => w.name === "image");
}
setPose(keypoints) {
this.canvas.clear();
this.canvas.backgroundColor = "#000";
const res = [];
for (let i = 0; i < keypoints.length; i += 18) {
const chunk = keypoints.slice(i, i + 18);
res.push(chunk);
}
for (let item of res) {
this.addPose(item);
this.canvas.discardActiveObject();
}
}
addPose(keypoints = undefined) {
if (keypoints === undefined) {
keypoints = default_keypoints;
}
const group = new fabric.Group();
const makeCircle = (
color,
left,
top,
line1,
line2,
line3,
line4,
line5
) => {
let c = new fabric.Circle({
left: left,
top: top,
strokeWidth: 1,
radius: 5,
fill: color,
stroke: color,
});
c.hasControls = c.hasBorders = false;
c.line1 = line1;
c.line2 = line2;
c.line3 = line3;
c.line4 = line4;
c.line5 = line5;
return c;
};
const makeLine = (coords, color) => {
return new fabric.Line(coords, {
fill: color,
stroke: color,
strokeWidth: 10,
selectable: false,
evented: false,
});
};
const lines = [];
const circles = [];
for (let i = 0; i < connect_keypoints.length; i++) {
// 接続されるidxを指定 [0, 1]なら0と1つなぐ
const item = connect_keypoints[i];
const line = makeLine(
keypoints[item[0]].concat(keypoints[item[1]]),
`rgba(${connect_color[i].join(", ")}, 0.7)`
);
lines.push(line);
this.canvas.add(line);
}
for (let i = 0; i < keypoints.length; i++) {
let list = [];
connect_keypoints.filter((item, idx) => {
if (item.includes(i)) {
list.push(lines[idx]);
return idx;
}
});
const circle = makeCircle(
`rgb(${connect_color[i].join(", ")})`,
keypoints[i][0],
keypoints[i][1],
...list
);
circle["id"] = i;
circles.push(circle);
group.addWithUpdate(circle);
}
this.canvas.discardActiveObject();
this.canvas.setActiveObject(group);
this.canvas.add(group);
group.toActiveSelection();
this.canvas.requestRenderAll();
}
initCanvas() {
this.canvas = new fabric.Canvas(this.canvas, {
backgroundColor: "#000",
preserveObjectStacking: true,
});
const updateLines = (target) => {
if ("_objects" in target) {
const flipX = target.flipX ? -1 : 1;
const flipY = target.flipY ? -1 : 1;
this.flipped = flipX * flipY === -1;
const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes;
if (target.angle === 0) {
const rtop = target.top;
const rleft = target.left;
for (const item of target._objects) {
let p = item;
p.scaleX = 1;
p.scaleY = 1;
const top =
rtop +
p.top * target.scaleY * flipY +
(target.height * target.scaleY) / 2;
const left =
rleft +
p.left * target.scaleX * flipX +
(target.width * target.scaleX) / 2;
p["_top"] = top;
p["_left"] = left;
if (p["id"] === 0) {
p.line1 && p.line1.set({ x1: left, y1: top });
} else {
p.line1 && p.line1.set({ x2: left, y2: top });
}
if (p["id"] === 14 || p["id"] === 15) {
p.radius = showEyes ? 5 : 0;
if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0;
if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0;
}
p.line2 && p.line2.set({ x1: left, y1: top });
p.line3 && p.line3.set({ x1: left, y1: top });
p.line4 && p.line4.set({ x1: left, y1: top });
p.line5 && p.line5.set({ x1: left, y1: top });
}
} else {
const aCoords = target.aCoords;
const center = {
x: (aCoords.tl.x + aCoords.br.x) / 2,
y: (aCoords.tl.y + aCoords.br.y) / 2,
};
const rad = (target.angle * Math.PI) / 180;
const sin = Math.sin(rad);
const cos = Math.cos(rad);
for (const item of target._objects) {
let p = item;
const p_top = p.top * target.scaleY * flipY;
const p_left = p.left * target.scaleX * flipX;
const left = center.x + p_left * cos - p_top * sin;
const top = center.y + p_left * sin + p_top * cos;
p["_top"] = top;
p["_left"] = left;
if (p["id"] === 0) {
p.line1 && p.line1.set({ x1: left, y1: top });
} else {
p.line1 && p.line1.set({ x2: left, y2: top });
}
if (p["id"] === 14 || p["id"] === 15) {
p.radius = showEyes ? 5 : 0.3;
if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0;
if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0;
}
p.line2 && p.line2.set({ x1: left, y1: top });
p.line3 && p.line3.set({ x1: left, y1: top });
p.line4 && p.line4.set({ x1: left, y1: top });
p.line5 && p.line5.set({ x1: left, y1: top });
}
}
} else {
var p = target;
if (p["id"] === 0) {
p.line1 && p.line1.set({ x1: p.left, y1: p.top });
} else {
p.line1 && p.line1.set({ x2: p.left, y2: p.top });
}
p.line2 && p.line2.set({ x1: p.left, y1: p.top });
p.line3 && p.line3.set({ x1: p.left, y1: p.top });
p.line4 && p.line4.set({ x1: p.left, y1: p.top });
p.line5 && p.line5.set({ x1: p.left, y1: p.top });
}
this.canvas.renderAll();
};
this.canvas.on("object:moving", (e) => {
updateLines(e.target);
});
this.canvas.on("object:scaling", (e) => {
updateLines(e.target);
this.canvas.renderAll();
});
this.canvas.on("object:rotating", (e) => {
updateLines(e.target);
this.canvas.renderAll();
});
this.canvas.on("object:modified", () => {
if (
this.lockMode ||
this.canvas.getActiveObject().type == "activeSelection"
)
return;
this.undo_history.push(this.getJSON());
this.redo_history.length = 0;
this.history_change = true;
this.uploadPoseFile(this.node.name);
});
if (!LS_Poses[this.node.name].undo_history.length) {
this.setPose(default_keypoints);
this.undo_history.push(this.getJSON());
}
return this.canvas;
}
undo() {
if (this.undo_history.length > 0) {
this.lockMode = true;
if (this.undo_history.length > 1)
this.redo_history.push(this.undo_history.pop());
const content = this.undo_history[this.undo_history.length - 1];
this.loadPreset(content);
this.canvas.renderAll();
this.lockMode = false;
this.history_change = true;
this.uploadPoseFile(this.node.name);
}
}
redo() {
if (this.redo_history.length > 0) {
this.lockMode = true;
const content = this.redo_history.pop();
this.undo_history.push(content);
this.loadPreset(content);
this.canvas.renderAll();
this.lockMode = false;
this.history_change = true;
this.uploadPoseFile(this.node.name);
}
}
resetCanvas() {
this.canvas.clear();
this.canvas.backgroundColor = "#000";
this.addPose();
}
updateHistoryData() {
if (this.history_change) {
LS_Poses[this.node.name].undo_history = this.undo_history;
LS_Poses[this.node.name].redo_history = this.redo_history;
LS_Save();
this.history_change = false;
}
}
uploadPoseFile(fileName) {
// Upload pose to temp folder ComfyUI
const uploadFile = async (blobFile) => {
try {
const resp = await fetch("/upload/image", {
method: "POST",
body: blobFile,
});
if (resp.status === 200) {
const data = await resp.json();
if (!this.image.options.values.includes(data.name)) {
this.image.options.values.push(data.name);
}
this.image.value = data.name;
this.updateHistoryData();
} else {
alert(resp.status + " - " + resp.statusText);
}
} catch (error) {
console.error(error);
}
};
this.canvas.lowerCanvasEl.toBlob(function (blob) {
let formData = new FormData();
formData.append("image", blob, fileName);
formData.append("overwrite", "true");
formData.append("type", "temp");
uploadFile(formData);
}, "image/png");
// - end
const callb = this.node.callback,
self = this;
this.image.callback = function () {
this.image.value = self.node.name;
if (callb) {
return callb.apply(this, arguments);
}
};
}
getJSON() {
const json = {
keypoints: this.canvas
.getObjects()
.filter((item) => {
if (item.type === "circle") return item;
})
.map((item) => {
return [Math.round(item.left), Math.round(item.top)];
}),
};
return json;
}
loadPreset(json) {
try {
if (json["keypoints"].length % 18 === 0) {
this.setPose(json["keypoints"]);
} else {
throw new Error("keypoints is invalid");
}
} catch (e) {
console.error(e);
}
}
}
// Create OpenPose widget
function createOpenPose(node, inputName, inputData, app) {
node.name = inputName;
const widget = {
type: "openpose",
name: `w${inputName}`,
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
const margin = 10,
visible = app.canvas.ds.scale > 0.5 && this.type === "openpose",
clientRectBound = ctx.canvas.getBoundingClientRect(),
transform = new DOMMatrix()
.scaleSelf(
clientRectBound.width / ctx.canvas.width,
clientRectBound.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y),
w = (widgetWidth - margin * 2 - 3) * transform.a;
Object.assign(this.openpose.style, {
left: `${transform.a * margin + transform.e}px`,
top: `${transform.d + transform.f}px`,
width: w + "px",
height: w + "px",
position: "absolute",
zIndex: app.graph._nodes.indexOf(node),
});
Object.assign(this.openpose.children[0].style, {
width: w + "px",
height: w + "px",
});
Object.assign(this.openpose.children[1].style, {
width: w + "px",
height: w + "px",
});
Array.from(this.openpose.children[2].children).forEach((element) => {
Object.assign(element.style, {
width: `${28.0 * transform.a}px`,
height: `${22.0 * transform.d}px`,
fontSize: `${transform.d * 10.0}px`,
});
element.hidden = !visible;
});
},
};
// Fabric canvas
let canvasOpenPose = document.createElement("canvas");
node.openPose = new OpenPose(node, canvasOpenPose);
node.openPose.canvas.setWidth(512);
node.openPose.canvas.setHeight(512);
let widgetCombo = node.widgets.filter((w) => w.type === "combo");
widgetCombo[0].value = node.name;
widget.openpose = node.openPose.canvas.wrapperEl;
widget.parent = node;
// Create elements undo, redo, clear history
let panelButtons = document.createElement("div"),
undoButton = document.createElement("button"),
redoButton = document.createElement("button"),
historyClearButton = document.createElement("button");
panelButtons.className = "panelButtons comfy-menu-btns";
undoButton.textContent = "⟲";
redoButton.textContent = "⟳";
historyClearButton.textContent = "✖";
undoButton.title = "Undo";
redoButton.title = "Redo";
historyClearButton.title = "Clear History";
undoButton.addEventListener("click", () => node.openPose.undo());
redoButton.addEventListener("click", () => node.openPose.redo());
historyClearButton.addEventListener("click", () => {
if (confirm(`Delete all pose history of a node "${node.name}"?`)) {
node.openPose.undo_history = [];
node.openPose.redo_history = [];
node.openPose.setPose(default_keypoints);
node.openPose.undo_history.push(node.openPose.getJSON());
node.openPose.history_change = true;
node.openPose.updateHistoryData();
}
});
panelButtons.appendChild(undoButton);
panelButtons.appendChild(redoButton);
panelButtons.appendChild(historyClearButton);
node.openPose.canvas.wrapperEl.appendChild(panelButtons);
document.body.appendChild(widget.openpose);
// Add buttons add, reset, undo, redo poses
node.addWidget("button", "Add pose", "add_pose", () => {
node.openPose.addPose();
});
node.addWidget("button", "Reset pose", "reset_pose", () => {
node.openPose.resetCanvas();
});
// Add customWidget to node
node.addCustomWidget(widget);
node.onRemoved = () => {
if (Object.hasOwn(LS_Poses, node.name)) {
delete LS_Poses[node.name];
LS_Save();
}
// When removing this node we need to remove the input from the DOM
for (let y in node.widgets) {
if (node.widgets[y].openpose) {
node.widgets[y].openpose.remove();
}
}
};
widget.onRemove = () => {
widget.openpose?.remove();
};
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 = graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "openpose")) {
wid.openpose.style.left = -8000 + "px";
wid.openpose.style.position = "absolute";
}
}
}
};
return { widget: widget };
}
window.LS_Poses = {};
function LS_Save() {
///console.log("Save:", LS_Poses);
localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses));
}
app.registerExtension({
name: "comfy.easyuse.poseEditor",
async init(app) {
// Any initial setup to run as soon as the page loads
let style = document.createElement("style");
style.innerText = `.panelButtons{
position: absolute;
padding: 4px;
display: flex;
gap: 4px;
flex-direction: column;
width: fit-content;
}
.panelButtons button:last-child{
border-color: var(--error-text);
color: var(--error-text) !important;
}
`;
document.head.appendChild(style);
},
async setup(app) {
let openPoseNode = app.graph._nodes.filter((wi) => wi.type == "easy poseEditor");
if (openPoseNode.length) {
openPoseNode.map((n) => {
console.log(`Setup PoseNode: ${n.name}`);
let widgetImage = n.widgets.find((w) => w.name == "image");
if (widgetImage && Object.hasOwn(LS_Poses, n.name)) {
let pose_ls = LS_Poses[n.name].undo_history;
n.openPose.loadPreset(
pose_ls.length > 0
? pose_ls[pose_ls.length - 1]
: { keypoints: default_keypoints }
);
}
});
}
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "easy poseEditor") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
let openPoseNode = app.graph._nodes.filter(
(wi) => {wi.type == "easy poseEditor"}
),
nodeName = `Pose_${openPoseNode.length}`,
nodeNamePNG = `${nodeName}.png`;
console.log(`Create PoseNode: ${nodeName}`);
LS_Poses =
localStorage.getItem("ComfyUI_Poses") &&
JSON.parse(localStorage.getItem("ComfyUI_Poses"));
if (!LS_Poses) {
localStorage.setItem("ComfyUI_Poses", JSON.stringify({}));
LS_Poses = JSON.parse(localStorage.getItem("ComfyUI_Poses"));
}
if (!Object.hasOwn(LS_Poses, nodeNamePNG)) {
LS_Poses[nodeNamePNG] = {
undo_history: [],
redo_history: [],
};
LS_Save();
}
createOpenPose.apply(this, [this, nodeNamePNG, {}, app]);
setTimeout(() => {
this.openPose.uploadPoseFile(nodeNamePNG);
}, 1);
this.setSize([530, 620]);
return r;
};
}
},
});

View File

@@ -0,0 +1,47 @@
import { api } from "../../../scripts/api.js";
// 全局Seed
function globalSeedHandler(event) {
let nodes = app.graph._nodes_by_id;
for(let i in nodes) {
let node = nodes[i];
if(node.type == 'easy globalSeed') {
if(node.widgets) {
const w = node.widgets.find((w) => w.name == 'value');
const last_w = node.widgets.find((w) => w.name == 'last_seed');
last_w.value = w.value;
w.value = event.detail.value;
}
}
else{
if(node.widgets) {
const w = node.widgets.find((w) => w.name == 'seed_num' || w.name == 'seed' || w.name == 'noise_seed');
if(w && event.detail.seed_map[node.id] != undefined) {
w.value = event.detail.seed_map[node.id];
}
}
}
}
}
api.addEventListener("easyuse-global-seed", globalSeedHandler);
const original_queuePrompt = api.queuePrompt;
async function queuePrompt_with_seed(number, { output, workflow }) {
workflow.seed_widgets = {};
for(let i in app.graph._nodes_by_id) {
let widgets = app.graph._nodes_by_id[i].widgets;
if(widgets) {
for(let j in widgets) {
if((widgets[j].name == 'seed_num' || widgets[j].name == 'seed' || widgets[j].name == 'noise_seed') && widgets[j].type != 'converted-widget')
workflow.seed_widgets[i] = parseInt(j);
}
}
}
return await original_queuePrompt.call(api, number, { output, workflow });
}
api.queuePrompt = queuePrompt_with_seed;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long