Merge pull request #103 from shadowcz007/v0.9.1-优化app模式

V0.9.1 优化app模式
This commit is contained in:
shadow
2024-01-02 23:56:18 +08:00
committed by GitHub
14 changed files with 3073 additions and 875 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]]
+139 -23
View File
@@ -302,6 +302,8 @@
color: #555;
}
select,
button,
input {
@@ -314,10 +316,18 @@
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>
@@ -410,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);
@@ -440,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();
@@ -870,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) => {
@@ -961,11 +983,96 @@
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>`;
@@ -1017,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);
}), keywords[0]);
selectTag.style = `background: none;
color: black; max-width: 300px;
border-bottom: 1px solid #acacac;
border-radius: 0;`
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;
@@ -1085,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
}
@@ -1110,16 +1220,18 @@
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) {
@@ -1167,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'));
})
@@ -1320,7 +1432,7 @@
element: output,
update: async function (type = "image", val, id) {
console.log(val, id)
if (val && type == "image" && output.querySelector(`#output_${id}`)) {
if (val && type == "image" && output.querySelector(`#output_${id} img`)) {
// if (output.querySelector(`#output_${id}`)) {
let im = await createImage(val)
@@ -1340,7 +1452,7 @@
}
if (val && 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`);
@@ -1519,7 +1631,7 @@
api.addEventListener('execution_start', ({ detail }) => {
console.log("execution_start", detail)
try {
ui.status.update(`execution_start_#${detail.node}`);
ui.status.update(`execution_start`);
} catch (error) {
}
@@ -1528,6 +1640,10 @@
api.api_base = ""
api.init();
// 外挂的UI
createAllColorInput();
const lightbox = new PhotoSwipeLightbox({
gallery: '.output_card',
@@ -1584,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);
@@ -1600,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];
+26 -27
View File
@@ -101,13 +101,17 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
options = node.widgets.filter(w => w.type === 'slider')[0].options
// 备选的keywords清单
let ks = getLocalData(`_mixlab_PromptSlide`)
let keywords=ks[id];
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,
@@ -161,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()
@@ -187,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)
@@ -218,6 +224,7 @@ async function save (json, download = false) {
seed, //控制是fixed 还是random
share_prefix,
link,
category,
filename: `${name}_${version}.json`
}
@@ -226,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)
}
}
@@ -335,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())
+1 -1
View File
@@ -2247,7 +2247,7 @@ const node = {
} else {
// 动态添加
const w = ComfyWidgets.STRING(
node,
this,
'prompts',
['STRING', { multiline: true }],
app
+133 -62
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()
@@ -719,7 +721,78 @@ app.registerExtension({
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
const apps = await get_my_app()
// console.log('apps',apps)
let apps_map = { '0': [] }
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)
}
}
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) {}
}
}
})
)
} 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) {}
}
}
})
}
})
}
}
// console.log('apps',apps_map, apps_opts,apps)
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments)
@@ -805,19 +878,22 @@ app.registerExtension({
const updateNodes = (ns, nd) => {
let appInfoNodes = {}
try {
let appInfo=app.graph._nodes
.filter(n => n.type === 'AppInfo')[0];
appInfoNodes[appInfo.id]=2;
for (const id of appInfo.widgets[1].value.split('\n')) {
if (id && id.trim() && parseInt(id)) {
appInfoNodes[id]=0;
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;
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) {
@@ -833,15 +909,21 @@ app.registerExtension({
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)'};
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';
}
if (appInfoNodes[nodeId] === 2) {
// appinfo
d.style.backgroundColor = '#326328'
d.style.color = '#ffffff'
d.style.borderColor = 'transparent'
}
d.addEventListener('click', () => {
// console.log('node')
@@ -874,19 +956,22 @@ app.registerExtension({
let nodesDivv = document.createElement('div')
let appInfoNodes = {}
try {
let appInfo=app.graph._nodes
.filter(n => n.type === 'AppInfo')[0];
appInfoNodes[appInfo.id]=2;
for (const id of appInfo.widgets[1].value.split('\n')) {
if (id && id.trim() && parseInt(id)) {
appInfoNodes[id]=0;
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;
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) {
@@ -903,16 +988,21 @@ app.registerExtension({
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)'};
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';
}
if (appInfoNodes[nodeId] === 2) {
// appinfo
d.style.backgroundColor = '#326328'
d.style.color = '#ffffff'
d.style.borderColor = 'transparent'
}
d.addEventListener('click', () => {
console.log('click')
@@ -955,26 +1045,7 @@ app.registerExtension({
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) {}
}
}
})
options:apps_opts
}
}
)
+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
+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