Compare commits

..
22 Commits
Author SHA1 Message Date
shadow cea1b69286 Merge pull request #103 from shadowcz007/v0.9.1-优化app模式
V0.9.1 优化app模式
2024-01-02 23:56:18 +08:00
shadowcz007 ab8aa69389 v0.9.1
web app可以设置分类,在comfyui右键菜单可以编辑更新web app

The web app can be configured with categories, and the web app can be edited and updated in the right-click menu of ComfyUI.

暂时支持8种节点作为界面上的输入节点:Load Image、CLIPTextEncode、PromptSlide、TextInput_、Color、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
2024-01-02 23:54:24 +08:00
shadowcz007 681491f1d0 v0.9.1 2024-01-02 23:51:19 +08:00
shadowcz007 c2fb815074 更新示例:TwinShot 2024-01-02 23:50:39 +08:00
shadowcz007 fffa14dc44 修复了randomprompt里的一个小bug 2024-01-02 17:48:41 +08:00
shadowcz007 df37166d42 Switch节点增加flat功能,可以把list里的某个元素取出来单独处理 2024-01-02 17:44:17 +08:00
shadowcz007 25fa3a8f6a 支持按照分类隔离应用 2024-01-02 16:19:01 +08:00
shadowcz007 b11507c5e6 样式 2024-01-02 15:11:29 +08:00
shadowcz007 f06d02489f app支持color组件 2024-01-02 14:59:59 +08:00
shadowcz007 c203af2f71 优化 2024-01-02 13:42:39 +08:00
shadowcz007 c715155a70 渐变节点 2024-01-02 13:25:48 +08:00
shadowcz007 8163133294 优化颜色选择器 2024-01-02 12:17:41 +08:00
shadowcz007 765be5dab4 1 2024-01-02 11:03:22 +08:00
shadowcz007 7c1523389d Update index.html 2024-01-02 09:53:25 +08:00
shadowcz007 7a2b1ba166 支持category 2024-01-02 09:32:46 +08:00
shadowcz007 45b4dcfcd0 Update index.html 2024-01-01 22:46:10 +08:00
shadowcz007 3b2e535566 add photoswipe 2024-01-01 22:34:58 +08:00
shadowcz007 db556d13a3 1 2024-01-01 21:33:54 +08:00
shadowcz007 a987063c68 nodes map - appinfo 2024-01-01 21:08:38 +08:00
shadowcz007 4ce30ef899 Update ui_mixlab.js 2024-01-01 20:33:00 +08:00
shadowcz007 d988282d98 增加种子生成模式切换 2024-01-01 20:24:12 +08:00
shadowcz007 695fdf7ceb update 2024-01-01 20:08:39 +08:00
18 changed files with 3521 additions and 1061 deletions
+4 -1
View File
@@ -4,10 +4,13 @@
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
- 支持多个web app 切换
- 发布为app的workflow,可以在右键里再次编辑了
- web app可以设置分类,在comfyui右键菜单可以编辑更新web app
- Support multiple web app switching.
- Add the AppInfo node, which allows you to transform the workflow into a web app by simple configuration.
- The workflow, which is now released as an app, can also be edited again by right-clicking.
- The web app can be configured with categories, and the web app can be edited and updated in the right-click menu of ComfyUI.
![](./assets/0-m-app.png)
@@ -26,7 +29,7 @@ APP-JSON:
- [image-to-image](./example/Image-to-Image_2.json)
- text-to-text
> 暂时支持6种节点作为界面上的输入节点:Load Image、CLIPTextEncode、TextInput_、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
> 暂时支持8种节点作为界面上的输入节点:Load Image、CLIPTextEncode、PromptSlide、TextInput_、Color、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT、VHS_VideoCombine
+79 -12
View File
@@ -133,8 +133,38 @@ def create_for_https():
return (crt,key)
# workflow 目录下的所有json
def read_workflow_json_files_all(folder_path):
print('#read_workflow_json_files_all',folder_path)
json_files = []
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.endswith('.json'):
json_files.append(os.path.join(root, file))
data = []
for file_path in json_files:
try:
with open(file_path) as json_file:
json_data = json.load(json_file)
creation_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
numeric_timestamp = creation_time.timestamp()
file_info = {
'filename': os.path.basename(file_path),
'category': os.path.dirname(file_path),
'data': json_data,
'date': numeric_timestamp
}
data.append(file_info)
except Exception as e:
print(e)
sorted_data = sorted(data, key=lambda x: x['date'], reverse=True)
return sorted_data
# workflow
def read_workflow_json_files(folder_path):
def read_workflow_json_files(folder_path ):
json_files = []
for filename in os.listdir(folder_path):
if filename.endswith('.json'):
@@ -169,29 +199,46 @@ def get_workflows():
workflows=read_workflow_json_files(workflow_path)
return workflows
def get_my_workflow_for_app(filename="my_workflow_app.json"):
def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=False):
app_path=os.path.join(current_path, "app")
if not os.path.exists(app_path):
os.mkdir(app_path)
category_path=os.path.join(app_path,category)
if not os.path.exists(category_path):
os.mkdir(category_path)
apps=[]
if filename==None:
data=read_workflow_json_files(app_path)
#TODO 支持目录内遍历
if is_all:
data=read_workflow_json_files_all(category_path)
else:
data=read_workflow_json_files(category_path)
i=0
for item in data:
# print(item)
try:
x=item["data"]
if i==0:
apps.append({
"filename":item["filename"],
# "category":item['category'],
"data":x,
"date":item["date"]
"date":item["date"],
})
else:
category=''
if 'category' in x['app']:
category=x['app']['category']
apps.append({
"filename":item["filename"],
"category":category,
"data":{
"app":{
"category":category,
"description":x['app']['description'],
"filename":(x['app']['filename'] if 'filename' in x['app'] else "") ,
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
@@ -205,7 +252,7 @@ def get_my_workflow_for_app(filename="my_workflow_app.json"):
except Exception as e:
print("发生异常:", str(e))
else:
app_workflow_path=os.path.join(app_path, filename)
app_workflow_path=os.path.join(category_path, filename)
# print('app_workflow_path: ',app_workflow_path)
try:
with open(app_workflow_path) as json_file:
@@ -216,17 +263,22 @@ def get_my_workflow_for_app(filename="my_workflow_app.json"):
except Exception as e:
print("发生异常:", str(e))
if len(apps)==1:
data=read_workflow_json_files(app_path)
if len(apps)==1 and category!='' and category!=None:
data=read_workflow_json_files(category_path)
for item in data:
x=item["data"]
# print(apps[0]['filename'] ,item["filename"])
if apps[0]['filename']!=item["filename"]:
category=''
if 'category' in x['app']:
category=x['app']['category']
apps.append({
"filename":item["filename"],
# "category":category,
"data":{
"app":{
"category":category,
"description":x['app']['description'],
"filename":(x['app']['filename'] if 'filename' in x['app'] else "") ,
"icon":(x['app']['icon'] if 'icon' in x['app'] else None),
@@ -245,11 +297,16 @@ def save_workflow_json(data):
json.dump(data, file)
return workflow_path
def save_workflow_for_app(data,filename="my_workflow_app.json"):
def save_workflow_for_app(data,filename="my_workflow_app.json",category=""):
app_path=os.path.join(current_path, "app")
if not os.path.exists(app_path):
os.mkdir(app_path)
app_workflow_path=os.path.join(app_path, filename)
category_path=os.path.join(app_path,category)
if not os.path.exists(category_path):
os.mkdir(category_path)
app_workflow_path=os.path.join(category_path, filename)
try:
output_str = json.dumps(data['output'])
@@ -371,17 +428,26 @@ async def mixlab_workflow_hander(request):
'file_path':file_path
}
elif data['task']=='save_app':
file_path=save_workflow_for_app(data['data'],data['filename'])
category=""
if "category" in data:
category=data['category']
file_path=save_workflow_for_app(data['data'],data['filename'],category)
result={
'status':'success',
'file_path':file_path
}
elif data['task']=='my_app':
filename=None
category=""
admin=False
if 'filename' in data:
filename=data['filename']
if 'category' in data:
category=data['category']
if 'admin' in data:
admin=data['admin']
result={
'data':get_my_workflow_for_app(filename),
'data':get_my_workflow_for_app(filename,category,admin),
'status':'success',
}
elif data['task']=='list':
@@ -438,7 +504,7 @@ PromptServer.add_routes=new_add_routes
# 导入节点
from .nodes.PromptNode import RandomPrompt,PromptSlide
from .nodes.ImageNode import NoiseImage,TransparentImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.ImageNode import NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,AreaToMask,SmoothMask,FeatheredMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
@@ -454,6 +520,7 @@ NODE_CLASS_MAPPINGS = {
"RandomPrompt":RandomPrompt,
"PromptSlide":PromptSlide,
"NoiseImage":NoiseImage,
"GradientImage":GradientImage,
"TransparentImage":TransparentImage,
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
+1
View File
@@ -4774,6 +4774,7 @@
"Color",
"CombineMasks_",
"EnhanceImage",
"GradientImage",
"FaceToMask",
"FeatheredMask",
"FloatingVideo",
+127 -1
View File
@@ -1,6 +1,7 @@
import numpy as np
import requests
import torch
# from PIL import Image, ImageDraw
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
from PIL.PngImagePlugin import PngInfo
import base64,os,random
@@ -155,6 +156,54 @@ def get_not_transparent_area(image):
return (x, y, w, h)
def generate_gradient_image(width, height, start_color_hex, end_color_hex):
image = Image.new('RGBA', (width, height))
draw = ImageDraw.Draw(image)
if len(start_color_hex) == 7:
start_color_hex += "FF"
if len(end_color_hex) == 7:
end_color_hex += "FF"
start_color_hex = start_color_hex.lstrip("#")
end_color_hex = end_color_hex.lstrip("#")
# 将十六进制颜色代码转换为RGBA元组,包括透明度
start_color = tuple(int(start_color_hex[i:i+2], 16) for i in (0, 2, 4, 6))
end_color = tuple(int(end_color_hex[i:i+2], 16) for i in (0, 2, 4, 6))
for y in range(height):
# 计算当前行的颜色
r = int(start_color[0] + (end_color[0] - start_color[0]) * y / height)
g = int(start_color[1] + (end_color[1] - start_color[1]) * y / height)
b = int(start_color[2] + (end_color[2] - start_color[2]) * y / height)
a = int(start_color[3] + (end_color[3] - start_color[3]) * y / height)
# 绘制当前行的渐变色
draw.line((0, y, width, y), fill=(r, g, b, a))
# Create a mask from the image's alpha channel
mask = image.split()[-1]
# Convert the mask to a black and white image
mask = mask.convert('L')
image=image.convert('RGB')
return (image, mask)
# 示例用法
# width = 500
# height = 200
# start_color_hex = 'FF0000FF' # 红色,完全不透明
# end_color_hex = '0000FFFF' # 蓝色,完全不透明
# gradient_image = generate_gradient_image(width, height, start_color_hex, end_color_hex)
# gradient_image.save('gradient_image.png')
# 读取不了分层
def load_psd(image):
layers=[]
@@ -863,7 +912,10 @@ class TransparentImage:
# result 里输出给下个节点的数据
# print('TransparentImage',len(images_rgb))
return {"ui":{"images": ui_images,"image_paths":image_paths},"result": (image_paths,images_rgb,images_rgba)}
class EnhanceImage:
@classmethod
@@ -1597,6 +1649,80 @@ class MergeLayers:
class GradientImage:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"width": ("INT",{
"default": 512,
"min": 1, # 最小值
"max": 8192, # 最大值
"step": 1, # 间隔
"display": "number" # 控件类型: 输入框 number、滑块 slider
}),
"height": ("INT",{
"default": 512,
"min": 1,
"max": 8192,
"step": 1,
"display": "number"
}),
"start_color_hex": ("STRING",{"multiline": False,"default": "#FFFFFF","dynamicPrompts": False}),
"end_color_hex": ("STRING",{"multiline": False,"default": "#000000","dynamicPrompts": False}),
},
}
# 输出的数据类型
RETURN_TYPES = ("IMAGE","MASK",)
# 运行时方法名称
FUNCTION = "run"
# 右键菜单目录
CATEGORY = "♾️Mixlab/image"
# 输入是否为列表
INPUT_IS_LIST = False
# 输出是否为列表
OUTPUT_IS_LIST = (False,False,)
def run(self,width,height,start_color_hex, end_color_hex):
im,mask=generate_gradient_image(width, height, start_color_hex, end_color_hex)
#获取临时目录:temp
output_dir = folder_paths.get_temp_directory()
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('tmp_', output_dir)
image_file = f"{filename}_{counter:05}.png"
image_path=os.path.join(full_output_folder, image_file)
# 保存图片
im.save(image_path,compress_level=6)
# 把PIL数据类型转为tensor
im=pil2tensor(im)
mask=pil2tensor(mask)
# 定义ui字段,数据将回传到web前端的 nodeType.prototype.onExecuted
# result是节点的输出
return {"ui":{"images": [{
"filename": image_file,
"subfolder": subfolder,
"type":"temp"
}]},"result": (im,mask,)}
class NoiseImage:
@classmethod
def INPUT_TYPES(s):
+22 -3
View File
@@ -79,6 +79,16 @@ font_files = get_font_files(r_directory)
# print(font_files)
def flatten_list(nested_list):
flat_list = []
for item in nested_list:
if isinstance(item, list):
flat_list.extend(flatten_list(item))
else:
flat_list.append(item)
return flat_list
class ColorInput:
@classmethod
def INPUT_TYPES(s):
@@ -439,6 +449,7 @@ class AppInfo:
}),
"share_prefix":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
"link":("STRING",{"multiline": False,"default": "https://","dynamicPrompts": False}),
"category":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
}
}
@@ -453,13 +464,13 @@ class AppInfo:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,name,image,input_ids,output_ids,description,version,share_prefix,link):
def run(self,name,image,input_ids,output_ids,description,version,share_prefix,link,category):
im=create_temp_file(image)
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link]}, "result": (image,)}
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category]}, "result": (image,)}
@@ -500,6 +511,7 @@ class SwitchByIndex:
"step": 1,
"display": "number"
}),
"flat": (['off',"on"],),
}
}
@@ -513,13 +525,20 @@ class SwitchByIndex:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self, A,B,index):
def run(self, A,B,index,flat):
flat=flat[0]
C=[]
index=index[0]
for a in A:
C.append(a)
for b in B:
C.append(b)
if flat=='on':
C=flatten_list(C)
if index>-1:
try:
C=[C[index]]
+348 -66
View File
@@ -6,6 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixlab APP</title>
<style>
body {
margin: 0;
padding: 0;
}
.app {
display: flex;
width: 90%;
@@ -20,6 +25,7 @@
color: black;
padding: 12px;
cursor: pointer;
margin: 8px 44px;
}
.apps .content {
@@ -33,7 +39,7 @@
margin: 12px;
display: flex;
flex-direction: row;
background: #f8f8f8;
background: #ffffff;
cursor: pointer;
user-select: none;
}
@@ -54,12 +60,14 @@
.apps .card p {
margin: 0;
padding: 0;
max-height: 35px;
overflow: hidden;
width: 98px;
}
.apps .card img {
width: auto;
height: 100%;
margin: 0px;
}
@@ -71,8 +79,8 @@
}
.apps .card .icon {
width: 120px;
height: 120px;
width: 48px;
height: 48px;
overflow: hidden;
background: #e3e3e3;
display: flex;
@@ -86,6 +94,20 @@
}
.card .toggle {
margin-left: 12px;
background: none;
color: black;
}
.card .toggle:hover {
color: #7f7f7f !important;
}
em .toggle:hover {
color: #7f7f7f !important;
}
.status_seed {
display: flex;
flex-direction: column;
@@ -177,7 +199,7 @@
.output_card img,
video {
max-width: 400px;
max-width: 200px;
max-height: 600px;
margin: 8px;
min-width: 200px;
@@ -188,7 +210,7 @@
background-color: #eee;
display: flex;
flex-direction: column;
padding: 24px;
padding: 8px;
font-size: 12px;
}
@@ -224,8 +246,8 @@
}
button:hover {
border-color: yellow;
color: yellow;
border-color: yellow !important;
color: yellow !important;
}
.disabled {
@@ -280,6 +302,8 @@
color: #555;
}
select,
button,
input {
@@ -292,22 +316,51 @@
border-radius: 4px;
margin-top: 8px;
}
.pickr .pcr-button {
width: 56px !important;
height: 56px !important;
outline: 1px solid white;
}
</style>
<!-- <script src="../../../scripts/api.js" type="module"></script> -->
<link href="/extensions/comfyui-mixlab-nodes/lib/photoswipe.min.css" rel="stylesheet">
<link href="/extensions/comfyui-mixlab-nodes/lib/classic.min.css" rel="stylesheet">
<script src="/extensions/comfyui-mixlab-nodes/lib/pickr.min.js"></script>
</head>
<body>
<div style="margin: 0 24px;
margin-bottom: 24px;
padding: 8px;
color: #4a4a4a;
border-bottom: 1px dashed #595959;">Explore your creative potential with <a class="link"
href="https://github.com/shadowcz007/comfyui-mixlab-nodes" target="_blank">mixlab-nodes</a> / 尽情发挥你的创意
<br>
<a class="link" href="https://www.mixcomfy.com" target="_blank">ComfyUI中文爱好者社区推荐</a>
<div style="display: flex;
align-items: center;
justify-content: space-around;">
<div style="margin: 0 24px;
margin-bottom: 24px;
padding: 8px;
color: #4a4a4a;
border-bottom: 1px dashed #595959;">Explore your creative potential with <a class="link"
href="https://github.com/shadowcz007/comfyui-mixlab-nodes" target="_blank">mixlab-nodes</a> / 尽情发挥你的创意
<br>
<a class="link" href="https://www.mixcomfy.com" target="_blank">ComfyUI中文爱好者社区推荐</a>
</div>
<a target="_blank" href="https://github.com/shadowcz007/comfyui-mixlab-nodes" style="text-decoration: none;
color: black;font-size:12px">
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true"
class="octicon octicon-mark-github v-align-middle color-fg-default">
<path
d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z">
</path>
</svg> Code
</a>
</div>
<div></div>
<!-- <script type="module" src="https://cdn.bootcdn.net/ajax/libs/photoswipe/5.4.0/photoswipe-lightbox.esm.min.js"></script> -->
<script type="module">
import PhotoSwipeLightbox from '/extensions/comfyui-mixlab-nodes/lib/photoswipe-lightbox.esm.min.js'
// console.log(Lightbox)
import { api } from "../../../scripts/api.js";
// console.log('api', api)
const base64Df =
@@ -354,7 +407,7 @@
&& !Array.isArray(data[id].inputs.seed) //如果是数组,则由其他节点控制
&& ['increment', 'decrement', 'randomize'].includes(seed[id])) {
data[id].inputs.seed = Math.round(Math.random() * 1849378600828930)
console.log('new Seed', data[id])
// console.log('new Seed', data[id])
}
}
return data
@@ -367,7 +420,12 @@
function queuePrompt(promptWorkflow, seed, client_id) {
// appinfo升级后 兼容,补丁
for (const id in promptWorkflow) {
if (promptWorkflow[id].class_type == 'AppInfo') {
promptWorkflow[id].inputs.category = promptWorkflow[id].inputs.category || ""
}
}
// 随机seed
promptWorkflow = randomSeed(seed, promptWorkflow);
@@ -397,13 +455,14 @@
}, 5000)
}
async function get_my_app(filename = null) {
async function get_my_app(category = "", filename = null) {
let url = get_url()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
task: 'my_app',
filename
filename,
category
})
})
let result = await res.json();
@@ -497,26 +556,46 @@
output_card.appendChild(div);
};
if (["SaveImage", "PreviewImage"].includes(node.class_type)) {
let a = document.createElement('a');
a.id = `output_${node.id}`
a.setAttribute('data-pswp-width', "200");
a.setAttribute('data-pswp-height', "200");
a.setAttribute('target', "_blank");
a.setAttribute('href', base64Df);
let img = new Image();
img.id = `output_${node.id}`;
// img;
img.src = base64Df;
output_card.appendChild(img);
a.appendChild(img)
output_card.appendChild(a);
}
// video ,gif
if (["VHS_VideoCombine"].includes(node.class_type)) {
let v = document.createElement('div');
let a = document.createElement('a');
a.id = `output_${node.id}`
a.setAttribute('data-pswp-width', "200");
a.setAttribute('data-pswp-height', "200");
a.setAttribute('target', "_blank");
a.setAttribute('href', base64Df);
// TODO 支持视频 https://photoswipe.com/custom-content/
// let v = document.createElement('div');
let video = document.createElement('video'), img = new Image();
video.style.display = 'none'
video.controls = 'true'
video.autoplay = 'true'
video.loop = 'true'
v.id = `output_${node.id}`;
// v.id = `output_${node.id}`;
img.src = base64Df;
v.appendChild(video);
v.appendChild(img);
output_card.appendChild(v);
a.appendChild(video);
a.appendChild(img);
output_card.appendChild(a);
}
}
@@ -807,7 +886,13 @@
max: 3,
};
const label = data.title == 'PromptSlide ♾️Mixlab' ? data.inputs.prompt_keyword : data.title
const label = data.title;
if (!options.keywords.includes(data.inputs.prompt_keyword)) {
options.keywords = [data.inputs.prompt_keyword, ...options.keywords]
};
// data.title == 'PromptSlide ♾️Mixlab' ? data.inputs.prompt_keyword : data.title
let silde = createNumSlide(label,
data.inputs.weight,
(v) => {
@@ -898,11 +983,124 @@
container.appendChild(div);
}
if (["Color"].includes(data.class_type)) {
let value = data.inputs.color.hex || '#000000';
let d = document.createElement('div');
d.className = 'card';
let label = document.createElement('label');
label.innerText = data.title;
let color = document.createElement('div');
color.id = `color_input_${data.id}`;
color.className = 'color_input';
color.setAttribute('data-color', value);
color.setAttribute('data-id', data.id);
d.appendChild(label);
d.appendChild(color);
container.appendChild(d);
}
});
return container
}
function createColorInput(elId, value, nodeId) {
const pickr = Pickr.create({
el: `#${elId}`,
theme: 'classic',
default: value,
swatches: [
'rgba(244, 67, 54, 1)',
'rgba(233, 30, 99, 0.95)',
'rgba(156, 39, 176, 0.9)',
'rgba(103, 58, 183, 0.85)',
'rgba(63, 81, 181, 0.8)',
'rgba(33, 150, 243, 0.75)',
'rgba(3, 169, 244, 0.7)',
'rgba(0, 188, 212, 0.7)',
'rgba(0, 150, 136, 0.75)',
'rgba(76, 175, 80, 0.8)',
'rgba(139, 195, 74, 0.85)',
'rgba(205, 220, 57, 0.9)',
'rgba(255, 235, 59, 0.95)',
'rgba(255, 193, 7, 1)'
],
components: {
// Main components
preview: true,
opacity: true,
hue: true,
// Input / output Options
interaction: {
hex: true,
rgba: true,
hsla: true,
hsva: true,
cmyk: true,
input: true,
// clear: true,
save: true,
cancel: true
}
}
});
pickr
.on('save', (color, instance) => {
try {
// console.log(color)
// window._appData.data[data.id].inputs.color.hex = color.toHEXA().toString();
let [r, g, b, a] = color.toRGBA();
window._appData.data[nodeId].inputs.color = {
...window._appData.data[nodeId].inputs.color,
r, g, b, a,
hex: color.toHEXA().toString()
}
} catch (error) { }
})
.on('cancel', instance => {
pickr && pickr.hide()
})
}
function createAllColorInput() {
for (const cInp of document.querySelectorAll('.color_input')) {
createColorInput(cInp.id, cInp.getAttribute('data-color'), cInp.getAttribute('data-id'));
}
}
function createToggleBtn() {
var toggleButton = document.createElement("button");
toggleButton.innerHTML = `<svg style="width:24px;height: 24px;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3972"><path d="M514 114.3c-219.9 0-398.9 178.9-398.9 398.8 0.1 220 179 398.9 398.9 398.9 219.9 0 398.8-178.9 398.8-398.8S733.9 114.3 514 114.3z m218.3 489v1.7c0 0.5-0.1 1-0.1 1.6 0 0.3 0 0.6-0.1 0.9 0 0.5-0.1 1-0.2 1.5 0 0.3-0.1 0.7-0.1 1-0.1 0.4-0.1 0.8-0.2 1.2-0.1 0.4-0.2 0.9-0.2 1.3-0.1 0.3-0.1 0.6-0.2 0.8-0.1 0.6-0.3 1.2-0.4 1.8 0 0.1-0.1 0.2-0.1 0.3-2.2 8.5-6.6 16.6-13.3 23.3L600.7 755.4c-20 20-52.7 20-72.6 0-20-20-20-52.7 0-72.6l28.9-28.9H347c-28.3 0-51.4-23.1-51.4-51.4 0-28.3 23.1-51.4 51.4-51.4h334c13.2 0 26.4 5 36.4 15s15 23.2 15 36.4c0 0.3-0.1 0.6-0.1 0.8z m0.1-179.5c0 28.3-23.1 51.4-51.4 51.4H347c-13.2 0-26.4-5-36.4-15s-15-23.2-15-36.4v-0.8-1.6c0-0.5 0.1-1.1 0.1-1.6 0-0.3 0-0.6 0.1-0.9 0-0.5 0.1-1 0.2-1.5 0-0.3 0.1-0.7 0.1-1 0.1-0.4 0.1-0.8 0.2-1.2 0.1-0.4 0.2-0.9 0.2-1.3 0.1-0.3 0.1-0.6 0.2-0.8 0.1-0.6 0.3-1.2 0.4-1.8 0-0.1 0.1-0.2 0.1-0.3 2.2-8.5 6.6-16.6 13.3-23.3l116.6-116.6c20-20 52.7-20 72.6 0 20 20 20 52.7 0 72.6L471 372.5h210c28.2 0 51.4 23.1 51.4 51.3z" p-id="3973"></path></svg>`;
toggleButton.className = 'toggle'
return toggleButton
}
function createCheckbox(defaultValue, labelText) {
let div = document.createElement('div');
// Create a checkbox element
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
// Set the default value
checkbox.checked = defaultValue;
// Create a label element
var label = document.createElement('label');
label.innerHTML = labelText;
// Append the checkbox and label to the body or any other container element
div.appendChild(checkbox);
div.appendChild(label);
return [div, checkbox]
}
function createNumSlide(labelText, value = 0, callback, minValue = 0, maxValue = 1, type = 'float', keywords = null, targetId = null) {
value = 'float' ? parseFloat(value.toFixed(3)) : parseInt(value)
@@ -926,18 +1124,20 @@
label.setAttribute('data-content', value);
if (keywords && keywords[0]) {
label.innerHTML = ``
// label.innerHTML = ``
// 有备选的关键词
let selectTag = createSelect(Array.from([labelText, ...keywords], k => {
let selectTag = createSelect(Array.from(keywords, (k,i) => {
return {
value: k,
text: k,
selected:i==0
}
}), labelText);
selectTag.style=`background: none;
color: black; max-width: 300px;
border-bottom: 1px solid #acacac;
border-radius: 0;`
}), keywords[0]);
selectTag.style = `background: none;
color: black; max-width: 300px;
border-bottom: 1px solid #acacac;
border-radius: 0;`
// selectTag.setAttribute('data-content',labelText);
selectTag.addEventListener('change', e => {
e.preventDefault();
window._appData.data[targetId].inputs.prompt_keyword = selectTag.value;
@@ -955,9 +1155,7 @@
// 创建切换按钮元素
var toggleButton = document.createElement("button");
toggleButton.innerHTML = `<svg style="width:24px;height: 24px;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3972"><path d="M514 114.3c-219.9 0-398.9 178.9-398.9 398.8 0.1 220 179 398.9 398.9 398.9 219.9 0 398.8-178.9 398.8-398.8S733.9 114.3 514 114.3z m218.3 489v1.7c0 0.5-0.1 1-0.1 1.6 0 0.3 0 0.6-0.1 0.9 0 0.5-0.1 1-0.2 1.5 0 0.3-0.1 0.7-0.1 1-0.1 0.4-0.1 0.8-0.2 1.2-0.1 0.4-0.2 0.9-0.2 1.3-0.1 0.3-0.1 0.6-0.2 0.8-0.1 0.6-0.3 1.2-0.4 1.8 0 0.1-0.1 0.2-0.1 0.3-2.2 8.5-6.6 16.6-13.3 23.3L600.7 755.4c-20 20-52.7 20-72.6 0-20-20-20-52.7 0-72.6l28.9-28.9H347c-28.3 0-51.4-23.1-51.4-51.4 0-28.3 23.1-51.4 51.4-51.4h334c13.2 0 26.4 5 36.4 15s15 23.2 15 36.4c0 0.3-0.1 0.6-0.1 0.8z m0.1-179.5c0 28.3-23.1 51.4-51.4 51.4H347c-13.2 0-26.4-5-36.4-15s-15-23.2-15-36.4v-0.8-1.6c0-0.5 0.1-1.1 0.1-1.6 0-0.3 0-0.6 0.1-0.9 0-0.5 0.1-1 0.2-1.5 0-0.3 0.1-0.7 0.1-1 0.1-0.4 0.1-0.8 0.2-1.2 0.1-0.4 0.2-0.9 0.2-1.3 0.1-0.3 0.1-0.6 0.2-0.8 0.1-0.6 0.3-1.2 0.4-1.8 0-0.1 0.1-0.2 0.1-0.3 2.2-8.5 6.6-16.6 13.3-23.3l116.6-116.6c20-20 52.7-20 72.6 0 20 20 20 52.7 0 72.6L471 372.5h210c28.2 0 51.4 23.1 51.4 51.3z" fill="#0C0C0C" p-id="3973"></path></svg>`;
toggleButton.style = `margin-left:12px;background:none;color: black;`
var toggleButton = createToggleBtn()
toggleButton.addEventListener("click", function () {
if (slider.type === 'range') {
@@ -996,11 +1194,12 @@
option.value = options[i].value;
option.text = options[i].text;
selectElement.appendChild(option);
// if(options[i].selected)
}
// 设置默认值
selectElement.value = defaultValue;
console.log(defaultValue,options)
return selectElement
}
@@ -1021,19 +1220,27 @@
return [div, selectElement];
}
function getFilenameFromUrl(url) {
function getFilenameAndCategoryFromUrl(url) {
const queryString = url.split('?')[1];
if (!queryString) {
return null;
return {};
}
const params = new URLSearchParams(queryString);
const filename = decodeURIComponent(params.get('filename'));
return filename;
const filename = params.get('filename') ? decodeURIComponent(params.get('filename')) : null;
const category = params.get('category') ? decodeURIComponent(params.get('category') || '') : '';
return { category, filename };
}
function createImage(url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
function createUI(data, share = true) {
// appData.input, appData.output, appData.seed, share, appData.link
@@ -1072,7 +1279,7 @@
shareBtn.innerText = 'copy url';
shareBtn.addEventListener('click', e => {
e.preventDefault();
let url = `${get_url()}/mixlab/app?filename=${encodeURIComponent(window._appData.filename)}`;
let url = `${get_url()}/mixlab/app?filename=${encodeURIComponent(window._appData.filename)}&category=${encodeURIComponent(window._appData.category || '')}`;
copyTextToClipboard(url, success(e, shareBtn, 'copy url'));
})
@@ -1120,14 +1327,34 @@
seedInput.style = `outline: 1px dashed gray;margin-bottom: 12px;margin-top: 12px;`
let em = document.createElement('em');
em.innerText = `#${id} ${s.toUpperCase()}`;
let emText = document.createElement('span');
emText.innerText = `#${id} ${s.toUpperCase()}`;
em.appendChild(emText);
seedInput.appendChild(em)
if (s === 'fixed') {
// console.log('###fiex',1)
let inSeed = createNumSlide(``, 0, (newSeed) => updateSeed(id, newSeed), 0, 1849378600828930, 'int');
inSeed.style = `padding: 8px;background: none;`
seedInput.appendChild(inSeed)
inSeed.style = `padding: 8px;background: none`
let toggleRandomize = createToggleBtn();
toggleRandomize.style = `background:none;color:black`;
toggleRandomize.addEventListener("click", function () {
if (data.seed[id] === 'randomize') {
data.seed[id] = 'fixed';
inSeed.style.display = 'block'
} else {
data.seed[id] = 'randomize';
inSeed.style.display = 'none'
}
emText.innerText = `#${id} ${data.seed[id].toUpperCase()}`;
});
seedInput.appendChild(inSeed);
em.appendChild(toggleRandomize);
}
}
@@ -1203,26 +1430,49 @@
},
output: {
element: output,
update: function (type = "image", val, id) {
update: async function (type = "image", val, id) {
console.log(val, id)
if (type == "image" && output.querySelector(`#output_${id}`)) {
if (output.querySelector(`#output_${id} img`)) {
output.querySelector(`#output_${id} img`).src = val;
} else {
output.querySelector(`#output_${id}`).src = val;
}
if (val && type == "image" && output.querySelector(`#output_${id} img`)) {
// if (output.querySelector(`#output_${id}`)) {
let im = await createImage(val)
output.querySelector(`#output_${id} img`).src = val;
let a = output.querySelector(`#output_${id}`);
a.setAttribute('data-pswp-width', im.naturalWidth);
a.setAttribute('data-pswp-height', im.naturalHeight);
a.setAttribute('target', "_blank");
a.setAttribute('href', val);
// }
// else {
// output.querySelector(`#output_${id}`).src = val;
// }
}
if (type == "video" && output.querySelector(`#output_${id}`)) {
if (val && type == "video" && output.querySelector(`#output_${id} video`)) {
let video = output.querySelector(`#output_${id} video`);
let img = output.querySelector(`#output_${id} img`);
img.style.display = 'none';
video.style.display = 'block';
video.onloadeddata = function () {
let a = output.querySelector(`#output_${id}`);
a.setAttribute('data-pswp-width', video.videoWidth);
a.setAttribute('data-pswp-height', video.videoHeight);
a.setAttribute('target', "_blank");
a.setAttribute('href', val);
};
video.src = val;
}
if (type == "text" && output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).innerText = val;
if (val && type == "text" && output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).innerText = val;
}
},
@@ -1371,15 +1621,44 @@
});
api.addEventListener("b_preview", ({ detail }) => {
// if (!enabled) return;
console.log("b_preview", detail)
show(URL.createObjectURL(detail));
});
// api.addEventListener("b_preview", ({ detail }) => {
// // if (!enabled) return;
// console.log("b_preview", detail)
// show(URL.createObjectURL(detail));
// });
api.addEventListener('execution_start', ({ detail }) => {
console.log("execution_start", detail)
try {
ui.status.update(`execution_start`);
} catch (error) {
}
})
api.api_base = ""
api.init();
// 外挂的UI
createAllColorInput();
const lightbox = new PhotoSwipeLightbox({
gallery: '.output_card',
children: 'a',
pswpModule: () => import('/extensions/comfyui-mixlab-nodes/lib/photoswipe.esm.min.js')
});
lightbox.init();
// window._lightbox = lightbox
// lightbox.addFilter('preventPointerEvent', (preventPointerEvent, originalEvent, pointerType) => {
// // return true to preventDefault pointermove/pointerdown events
// // (also applies to touchmove/mousemove)
// return true;
// });
}
// 创建app的选择菜单
@@ -1421,7 +1700,7 @@
dd.className = 'card selected'
// console.log(app.filename)
window._appData = (await get_my_app(app.filename))[0];
window._appData = (await get_my_app(app.category, app.filename))[0];
if (document.body.querySelector('.app')) document.body.querySelector('.app').remove()
createApp(window._appData);
@@ -1437,8 +1716,8 @@
async function init_app() {
const filename = getFilenameFromUrl(location.href);
window._apps = await get_my_app(filename);
const { category, filename } = getFilenameAndCategoryFromUrl(location.href);
window._apps = await get_my_app(category, filename);
window._appData = window._apps[0];
@@ -1446,10 +1725,13 @@
createApp(window._appData);
};
init_app()
</script>
</body>
</html>
+27 -27
View File
@@ -100,13 +100,18 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
// min max step
options = node.widgets.filter(w => w.type === 'slider')[0].options
// 备选的keywords清单
let keywords=getLocalData(`${id}_PromptSlide`);
let ks = getLocalData(`_mixlab_PromptSlide`)
let keywords = ks[id]
// console.log('keywords',keywords)
if(keywords&&keywords[0]){
options.keywords=keywords;
if (keywords && keywords[0]) {
options.keywords = keywords
}
}
if(node.type=='Color'){
}
input[inputIds.indexOf(id)] = {
...data[id],
title: node.title,
@@ -160,7 +165,8 @@ async function save_app (json) {
body: JSON.stringify({
data: json,
task: 'save_app',
filename: json.app.filename
filename: json.app.filename,
category: json.app.category
})
})
return await res.json()
@@ -186,7 +192,8 @@ async function save (json, download = false) {
const name = json[0],
version = json[5],
share_prefix = json[6], //用于分享的功能扩展
link=json[7],//用于创建界面上的跳转链接
link = json[7], //用于创建界面上的跳转链接
category = json[8] || '', //用于分类
description = json[4],
inputIds = json[2].split('\n').filter(f => f),
outputIds = json[3].split('\n').filter(f => f)
@@ -217,6 +224,7 @@ async function save (json, download = false) {
seed, //控制是fixed 还是random
share_prefix,
link,
category,
filename: `${name}_${version}.json`
}
@@ -225,31 +233,23 @@ async function save (json, download = false) {
} catch (error) {}
// console.log(data.app)
// let http_workflow = app.graph.serialize()
await save_app(data)
if (download) {
await save_app(data)
await downloadJsonFile(data, data.app.filename)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}`
)
if (open)
window.open(
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}`
)
} else {
await save_app(data)
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app`
)
if (open) window.open(`${getUrl()}/mixlab/app`)
}
let open = window.confirm(
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}&category=${encodeURIComponent(data.app.category)}`
)
if (open)
window.open(
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(
data.app.filename
)}&category=${encodeURIComponent(data.app.category)}`
)
} catch (error) {
console.log('###SpeechRecognition', error)
console.log('###error', error)
}
}
@@ -334,7 +334,7 @@ app.registerExtension({
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = async function (message) {
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log(message.json)
window._mixlab_app_json = message.json
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.9.0'
const version = 'v0.9.1'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+23 -14
View File
@@ -1800,15 +1800,19 @@ const updateUI = node => {
pw.inputEl.title = `Total of ${prompts.length} prompts`
} else {
// 动态添加
console.log('ComfyWidgets',ComfyWidgets.STRING(
node,
'prompts',
['STRING', { multiline: true }]
))
// console.log('ComfyWidgets',ComfyWidgets.STRING(
// node,
// 'prompts',
// ['STRING', { multiline: true }]
// ))
// ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app)
const w = ComfyWidgets.STRING(
node,
'prompts',
['STRING', { multiline: true }]
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
@@ -2089,13 +2093,13 @@ const node = {
name: 'RandomPrompt',
async init (app) {
// Any initial setup to run as soon as the page loads
console.log('[logging]', 'extension init')
// console.log('[logging]', 'extension init')
if (window.location.href.match('/?')) {
const { workflow } = getURLParameters(window.location.href)
if (workflow)
get_my_workflow().then(data => {
console.log('#get_my_workflow', data)
// console.log('#get_my_workflow', data)
let my_workflow = data.filter(
d => d.filename == 'my_workflow.json'
)[0]
@@ -2131,10 +2135,15 @@ const node = {
// }
},
loadedGraphNode (node, app) {
// Fires for each node when loading/dragging/etc a workflow json or png
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
// console.log("[logging]", "loaded graph node: ", exportGraph(node.graph));
if (node.type === 'RandomPrompt') {
try {
let max_count = node.widgets.filter(w => w.name === "max_count")[0];
max_count.value= node.widgets_values[0]
// console.log('RandomPrompt',max_count,node.widgets_values[0])
} catch (error) {
console.log(error)
}
}
},
async nodeCreated (node) {
if (node.type === 'RandomPrompt') {
@@ -2227,7 +2236,7 @@ const node = {
const r = onExecuted?.apply?.(this, arguments)
let prompts = message.prompts
console.log('executed', message)
// console.log('executed', message)
// console.log('#RandomPrompt', this.widgets)
const pw = this.widgets.filter(w => w.name === 'prompts')[0]
@@ -2238,7 +2247,7 @@ const node = {
} else {
// 动态添加
const w = ComfyWidgets.STRING(
node,
this,
'prompts',
['STRING', { multiline: true }],
app
+7 -4
View File
@@ -138,7 +138,7 @@ app.registerExtension({
const reader = new FileReader()
// 定义读取完成事件的回调函数
reader.onload = (event)=> {
reader.onload = event => {
// 读取完成后的文本内容
const fileContent = event.target.result.split('\n')
const keywords = Array.from(fileContent, f => f.trim()).filter(
@@ -148,7 +148,9 @@ app.registerExtension({
// console.log(keywords)
// widget.value = keywords
setLocalDataOfWin(`${this.id}_PromptSlide`,keywords)
let ks = getLocalData(`_mixlab_PromptSlide`)
ks[this.id] = keywords
setLocalDataOfWin(`_mixlab_PromptSlide`, ks)
createSelect(select, keywords, prompt_keyword)
@@ -183,8 +185,9 @@ app.registerExtension({
if (node.type === 'PromptSlide') {
try {
let prompt = node.widgets.filter(w => w.name === 'prompt_keyword')[0]
let keywords= getLocalData( `${node.id}_PromptSlide`)
let ks = getLocalData(`_mixlab_PromptSlide`)
let keywords = ks[node.id]
// console.log('keywords',keywords)
let widget = node.widgets.filter(w => w.select)[0]
if (keywords && keywords[0]) {
+331 -188
View File
@@ -55,13 +55,15 @@ function get_url () {
return url
}
async function get_my_app (filename = null) {
async function get_my_app (filename = null, category = '') {
let url = get_url()
const res = await fetch(`${url}/mixlab/workflow`, {
method: 'POST',
body: JSON.stringify({
task: 'my_app',
filename
filename,
category,
admin: true
})
})
let result = await res.json()
@@ -713,202 +715,343 @@ app.registerExtension({
this.setDirty(true, true)
}
},
async setup () {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
setup () {
setTimeout(async () => {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
const apps = await get_my_app()
// console.log('apps',apps)
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments)
const apps = await get_my_app()
options.push(null, {
content: `Nodes Map ♾️Mixlab`,
disabled: false,
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
let apps_map = { '0': [] }
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
for (const app of apps) {
if (app.category) {
if (!apps_map[app.category]) apps_map[app.category] = []
apps_map[app.category].push(app)
} else {
apps_map['0'].push(app)
}
}
// console.log('[Mixlab]', 'loaded graph node: ', app)
let div =
document.querySelector('#mixlab_find_the_node') ||
document.createElement('div')
div.id = 'mixlab_find_the_node'
div.style = `
flex-direction: column;
align-items: end;
display:flex;position: absolute;
top: 50px; left: 50px; width: 200px;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
padding: 10px;
border: 1px solid black;z-index: 999999999;padding-top: 0;`
div.innerHTML = ''
let btn = document.createElement('div')
btn.style = `display: flex;
width: calc(100% - 24px);
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 44px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.style.fontSize = '12px'
textB.innerText = `Locate and navigate nodes ♾️Mixlab`
btnB.style = `float: right; border: none; color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
btnB.addEventListener('click', () => {
div.style.display = 'none'
})
btnB.innerText = 'X'
// 悬浮框拖动事件
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)
}
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
})
div.appendChild(btn)
const updateNodes = (ns, nd) => {
for (let nodeId in ns) {
let n = ns[nodeId].class_type
if (nodesMap[n]) {
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
console.log('node')
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, ns)) {
nd.innerHTML = ''
updateNodes(n, nd)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nd.appendChild(d)
let apps_opts = []
for (const category in apps_map) {
console.log('category',typeof(category))
if (category === '0') {
apps_opts.push(
...Array.from(apps_map[category], a => {
return {
content: a.name,
has_submenu: false,
callback: async () => {
try {
let item = (await get_my_app(a.filename))[0]
if (item) {
// console.log(item.data)
app.loadGraphData(item.data)
setTimeout(() => {
const node = app.graph._nodes_in_order[0]
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.5)
}, 1000)
}
} catch (error) {}
}
}
}
}
let nodesDivv = document.createElement('div')
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
if (nodesMap[n]) {
const { url, title: _title } = nodesMap[n]
let title = app.graph.getNodeById(nodeId).title || _title
let d = document.createElement('button')
d.style = `text-align: left;margin:6px;color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
d.addEventListener('click', () => {
console.log('click')
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, nodes)) {
nodesDivv.innerHTML = ''
updateNodes(n, nodesDivv)
})
)
} else {
// 二级
apps_opts.push({
content: '🚀 '+category,
has_submenu: true,
disabled: false,
submenu: {
options: Array.from(apps_map[category], a => {
return {
content: a.name,
callback: async () => {
try {
let item = (await get_my_app(a.filename, a.category))[0]
if (item) {
// console.log(item.data)
app.loadGraphData(item.data)
setTimeout(() => {
const node = app.graph._nodes_in_order[0]
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.5)
}, 1000)
}
} catch (error) {}
}
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${title}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = n
nodesDiv.appendChild(d)
}
}
nodesDivv.appendChild(nodesDiv)
nodesDivv.style = `overflow: scroll;
height: 70vh;width: 100%;`
div.appendChild(nodesDivv)
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
},{
content: 'Workflow App ♾️Mixlab',
has_submenu: true,
disabled: false,
submenu: {
options: Array.from(apps, a => {
return {
content: a.name,
callback: async () => {
try {
let item = (await get_my_app(a.filename))[0]
if (item) {
// console.log(item.data)
app.loadGraphData(item.data)
setTimeout(() => {
const node = app.graph._nodes_in_order[0]
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.5)
}, 1000)
}
} catch (error) {}
}
}
})
}
})
}
return options
}
// console.log('apps',apps_map, apps_opts,apps)
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments)
options.push(
null,
{
content: `Nodes Map ♾️Mixlab`,
disabled: false,
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
// console.log('[Mixlab]', 'loaded graph node: ', app)
let div =
document.querySelector('#mixlab_find_the_node') ||
document.createElement('div')
div.id = 'mixlab_find_the_node'
div.style = `
flex-direction: column;
align-items: end;
display:flex;position: absolute;
top: 50px; left: 50px; width: 200px;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
padding: 10px;
border: 1px solid black;z-index: 999999999;padding-top: 0;`
div.innerHTML = ''
let btn = document.createElement('div')
btn.style = `display: flex;
width: calc(100% - 24px);
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 44px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.style.fontSize = '12px'
textB.innerText = `Locate and navigate nodes ♾️Mixlab`
btnB.style = `float: right; border: none; color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
btnB.addEventListener('click', () => {
div.style.display = 'none'
})
btnB.innerText = 'X'
// 悬浮框拖动事件
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)
}
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
})
div.appendChild(btn)
const updateNodes = (ns, nd) => {
let appInfoNodes = {}
try {
let appInfo = app.graph._nodes.filter(
n => n.type === 'AppInfo'
)[0]
if (appInfo) {
appInfoNodes[appInfo.id] = 2
for (const id of appInfo.widgets[1].value.split('\n')) {
if (id && id.trim() && parseInt(id)) {
appInfoNodes[id] = 0
}
}
for (const id of app.graph._nodes
.filter(n => n.type === 'AppInfo')[0]
.widgets[2].value.split('\n')) {
if (id && id.trim() && parseInt(id)) {
appInfoNodes[id] = 1
}
}
}
} catch (error) {
console.log(error)
}
for (let nodeId in ns) {
let n = ns[nodeId].title || ns[nodeId].class_type
if (nodesMap[n]) {
const { url, title } = nodesMap[n]
let d = document.createElement('button')
d.style = `text-align: left;
margin:6px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: ${
appInfoNodes[nodeId] >= 0
? appInfoNodes[nodeId] === 1
? 'blue'
: 'red'
: 'var(--border-color)'
};
cursor: pointer;`
if (appInfoNodes[nodeId] === 2) {
// appinfo
d.style.backgroundColor = '#326328'
d.style.color = '#ffffff'
d.style.borderColor = 'transparent'
}
d.addEventListener('click', () => {
// console.log('node')
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, ns)) {
nd.innerHTML = ''
updateNodes(n, nd)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${n}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = title
nd.appendChild(d)
}
}
}
let nodesDivv = document.createElement('div')
let appInfoNodes = {}
try {
let appInfo = app.graph._nodes.filter(
n => n.type === 'AppInfo'
)[0]
if (appInfo) {
appInfoNodes[appInfo.id] = 2
for (const id of appInfo.widgets[1].value.split('\n')) {
if (id && id.trim() && parseInt(id)) {
appInfoNodes[id] = 0
}
}
for (const id of app.graph._nodes
.filter(n => n.type === 'AppInfo')[0]
.widgets[2].value.split('\n')) {
if (id && id.trim() && parseInt(id)) {
appInfoNodes[id] = 1
}
}
}
} catch (error) {
console.log(error)
}
for (let nodeId in nodes) {
let n = nodes[nodeId].class_type
if (nodesMap[n]) {
const { url, title: _title } = nodesMap[n]
let title = app.graph.getNodeById(nodeId).title || _title
let d = document.createElement('button')
d.style = `text-align: left;
margin:6px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: ${
appInfoNodes[nodeId] >= 0
? appInfoNodes[nodeId] === 1
? 'blue'
: 'red'
: 'var(--border-color)'
};
cursor: pointer;`
if (appInfoNodes[nodeId] === 2) {
// appinfo
d.style.backgroundColor = '#326328'
d.style.color = '#ffffff'
d.style.borderColor = 'transparent'
}
d.addEventListener('click', () => {
console.log('click')
const node = app.graph.getNodeById(nodeId)
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(1)
})
d.addEventListener('mouseover', async () => {
// console.log('mouseover')
let n = (await app.graphToPrompt()).output
if (!deepEqual(n, nodes)) {
nodesDivv.innerHTML = ''
updateNodes(n, nodesDivv)
}
})
d.innerHTML = `
<span>${'#' + nodeId} ${title}</span>
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
`
d.title = n
nodesDiv.appendChild(d)
}
}
nodesDivv.appendChild(nodesDiv)
nodesDivv.style = `overflow: scroll;
height: 70vh;width: 100%;`
div.appendChild(nodesDivv)
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
},
{
content: 'Workflow App ♾️Mixlab',
has_submenu: true,
disabled: false,
submenu: {
options:apps_opts
}
}
)
return options
}
}, 1000)
}
})
+112 -39
View File
@@ -57,11 +57,35 @@ function hexToRGBA (hexColor) {
// 将透明度的十六进制值转换为十进制值
var alpha = parseInt(alphaHex, 16) / 255
return [r,g,b,alpha]
return [r, g, b, alpha]
}
app.registerExtension({
name: 'Mixlab.utils.Color',
init () {
$el('link', {
rel: 'stylesheet',
href: '/extensions/comfyui-mixlab-nodes/lib/classic.min.css',
parent: document.head
})
$el('style', {
textContent: `
.pickr{
display: flex;
justify-content: center;
align-items: center;
}
.pickr .pcr-button {
width: 56px;
height: 56px;
outline: 1px solid white;
}
`,
parent: document.body
})
},
async getCustomWidgets (app) {
return {
TCOLOR (node, inputName, inputData, app) {
@@ -75,9 +99,10 @@ app.registerExtension({
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_utils_color');
let hex=data[node.id] || '#000000'
let [r,g,b,a]=hexToRGBA(hex)
// let data = getLocalData('_mixlab_utils_color')
// let hex = data[node.id] || '#000000'
let hex = widget.value || '#000000'
let [r, g, b, a] = hexToRGBA(hex)
return {
hex,
r,
@@ -100,7 +125,7 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
console.log('Color nodeData', this.widgets)
// console.log('Color nodeData', this.widgets)
const widget = {
type: 'div',
@@ -110,6 +135,7 @@ app.registerExtension({
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
)
// console.log('draw',y,node.widgets[0].last_y)
}
}
@@ -117,35 +143,9 @@ app.registerExtension({
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder, value) => {
const inputDiv = () => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'color'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 70%;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
// console.log(this.id, ip.value.trim())
})
div.id = `color_picker_${this.id}`
return div
}
@@ -155,10 +155,85 @@ app.registerExtension({
this.addCustomWidget(widget)
const pickr = Pickr.create({
el: `#${inputColor.id}`,
theme: 'classic', // or 'monolith', or 'nano'
// closeOnScroll: true,
default:'#000000',
swatches: [
'rgba(244, 67, 54, 1)',
'rgba(233, 30, 99, 0.95)',
'rgba(156, 39, 176, 0.9)',
'rgba(103, 58, 183, 0.85)',
'rgba(63, 81, 181, 0.8)',
'rgba(33, 150, 243, 0.75)',
'rgba(3, 169, 244, 0.7)',
'rgba(0, 188, 212, 0.7)',
'rgba(0, 150, 136, 0.75)',
'rgba(76, 175, 80, 0.8)',
'rgba(139, 195, 74, 0.85)',
'rgba(205, 220, 57, 0.9)',
'rgba(255, 235, 59, 0.95)',
'rgba(255, 193, 7, 1)'
],
components: {
// Main components
preview: true,
opacity: true,
hue: true,
// Input / output Options
interaction: {
hex: true,
rgba: true,
hsla: true,
hsva: true,
cmyk: true,
input: true,
// clear: true,
save: true,
cancel: true
}
}
})
pickr
.on('save', (color, instance) => {
// console.log('Event: "save"', color.toHEXA().toString())
// let data = getLocalData('_mixlab_utils_color')
// data[this.id] = color.toHEXA().toString()
// localStorage.setItem('_mixlab_utils_color', JSON.stringify(data))
try {
let tc = this.widgets.filter(w => w.type == 'TCOLOR')[0]
tc.value = color.toHEXA().toString()
} catch (error) {}
})
.on('cancel', instance => {
pickr && pickr.hide()
})
this.pickr = pickr
const handleMouseWheel = () => {
try {
this.pickr && this.pickr.hide()
} catch (error) {}
}
document.addEventListener('wheel', handleMouseWheel)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputColor.remove()
widget.div.remove()
try {
this.pickr.destroyAndRemove()
this.pickr = null
document.removeEventListener('wheel', handleMouseWheel)
} catch (error) {
console.log(error)
}
return onRemoved?.()
}
@@ -171,13 +246,11 @@ app.registerExtension({
// You can modify widgets/add handlers/etc here
if (node.type === 'Color') {
let widget = node.widgets.filter(w => w.div)[0]
try {
let TCOLOR = node.widgets.filter(w => w.type == 'TCOLOR')[0]
let data = getLocalData('_mixlab_utils_color')
let id = node.id
widget.div.querySelector('.Color').value = data[id] || '#000000'
setTimeout(() => node.pickr.setColor(TCOLOR.value || '#000000'), 1000)
} catch (error) {}
}
}
})
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */.pswp{--pswp-bg:#000;--pswp-placeholder-bg:#222;--pswp-root-z-index:100000;--pswp-preloader-color:rgba(79, 79, 79, 0.4);--pswp-preloader-color-secondary:rgba(255, 255, 255, 0.9);--pswp-icon-color:#fff;--pswp-icon-color-secondary:#4f4f4f;--pswp-icon-stroke-color:#4f4f4f;--pswp-icon-stroke-width:2px;--pswp-error-text-color:var(--pswp-icon-color)}.pswp{position:fixed;top:0;left:0;width:100%;height:100%;z-index:var(--pswp-root-z-index);display:none;touch-action:none;outline:0;opacity:.003;contain:layout style size;-webkit-tap-highlight-color:transparent}.pswp:focus{outline:0}.pswp *{box-sizing:border-box}.pswp img{max-width:none}.pswp--open{display:block}.pswp,.pswp__bg{transform:translateZ(0);will-change:opacity}.pswp__bg{opacity:.005;background:var(--pswp-bg)}.pswp,.pswp__scroll-wrap{overflow:hidden}.pswp__bg,.pswp__container,.pswp__content,.pswp__img,.pswp__item,.pswp__scroll-wrap,.pswp__zoom-wrap{position:absolute;top:0;left:0;width:100%;height:100%}.pswp__img,.pswp__zoom-wrap{width:auto;height:auto}.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img{cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.pswp--click-to-zoom.pswp--zoomed-in .pswp__img{cursor:move;cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active{cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,.pswp__img{cursor:-webkit-zoom-out;cursor:-moz-zoom-out;cursor:zoom-out}.pswp__button,.pswp__container,.pswp__counter,.pswp__img{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pswp__item{z-index:1;overflow:hidden}.pswp__hidden{display:none!important}.pswp__content{pointer-events:none}.pswp__content>*{pointer-events:auto}.pswp__error-msg-container{display:grid}.pswp__error-msg{margin:auto;font-size:1em;line-height:1;color:var(--pswp-error-text-color)}.pswp .pswp__hide-on-close{opacity:.005;will-change:opacity;transition:opacity var(--pswp-transition-duration) cubic-bezier(.4,0,.22,1);z-index:10;pointer-events:none}.pswp--ui-visible .pswp__hide-on-close{opacity:1;pointer-events:auto}.pswp__button{position:relative;display:block;width:50px;height:60px;padding:0;margin:0;overflow:hidden;cursor:pointer;background:0 0;border:0;box-shadow:none;opacity:.85;-webkit-appearance:none;-webkit-touch-callout:none}.pswp__button:active,.pswp__button:focus,.pswp__button:hover{transition:none;padding:0;background:0 0;border:0;box-shadow:none;opacity:1}.pswp__button:disabled{opacity:.3;cursor:auto}.pswp__icn{fill:var(--pswp-icon-color);color:var(--pswp-icon-color-secondary)}.pswp__icn{position:absolute;top:14px;left:9px;width:32px;height:32px;overflow:hidden;pointer-events:none}.pswp__icn-shadow{stroke:var(--pswp-icon-stroke-color);stroke-width:var(--pswp-icon-stroke-width);fill:none}.pswp__icn:focus{outline:0}.pswp__img--with-bg,div.pswp__img--placeholder{background:var(--pswp-placeholder-bg)}.pswp__top-bar{position:absolute;left:0;top:0;width:100%;height:60px;display:flex;flex-direction:row;justify-content:flex-end;z-index:10;pointer-events:none!important}.pswp__top-bar>*{pointer-events:auto;will-change:opacity}.pswp__button--close{margin-right:6px}.pswp__button--arrow{position:absolute;top:0;width:75px;height:100px;top:50%;margin-top:-50px}.pswp__button--arrow:disabled{display:none;cursor:default}.pswp__button--arrow .pswp__icn{top:50%;margin-top:-30px;width:60px;height:60px;background:0 0;border-radius:0}.pswp--one-slide .pswp__button--arrow{display:none}.pswp--touch .pswp__button--arrow{visibility:hidden}.pswp--has_mouse .pswp__button--arrow{visibility:visible}.pswp__button--arrow--prev{right:auto;left:0}.pswp__button--arrow--next{right:0}.pswp__button--arrow--next .pswp__icn{left:auto;right:14px;transform:scale(-1,1)}.pswp__button--zoom{display:none}.pswp--zoom-allowed .pswp__button--zoom{display:block}.pswp--zoomed-in .pswp__zoom-icn-bar-v{display:none}.pswp__preloader{position:relative;overflow:hidden;width:50px;height:60px;margin-right:auto}.pswp__preloader .pswp__icn{opacity:0;transition:opacity .2s linear;animation:pswp-clockwise .6s linear infinite}.pswp__preloader--active .pswp__icn{opacity:.85}@keyframes pswp-clockwise{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.pswp__counter{height:30px;margin-top:15px;margin-inline-start:20px;font-size:14px;line-height:30px;color:var(--pswp-icon-color);text-shadow:1px 1px 3px var(--pswp-icon-color-secondary);opacity:.85}.pswp--one-slide .pswp__counter{display:none}
+3
View File
File diff suppressed because one or more lines are too long
+2423 -705
View File
File diff suppressed because it is too large Load Diff