Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a46647d8c | ||
|
|
406a255db0 | ||
|
|
574557810e | ||
|
|
998a02c3a4 | ||
|
|
c6f964c921 | ||
|
|
efb0e147c5 | ||
|
|
9cf7356f98 | ||
|
|
af05c43174 | ||
|
|
380c68ff2b | ||
|
|
068b00b99f | ||
|
|
cd6a42ab64 | ||
|
|
f115abec92 | ||
|
|
f0e23cf878 | ||
|
|
a94f11d809 | ||
|
|
38972bea5f | ||
|
|
a761ff552a | ||
|
|
dbeb84ea9a | ||
|
|
0a4938f39a | ||
|
|
b2182c716d | ||
|
|
8253be73f6 | ||
|
|
20318e296e | ||
|
|
cea1b69286 | ||
|
|
ab8aa69389 | ||
|
|
681491f1d0 | ||
|
|
c2fb815074 | ||
|
|
fffa14dc44 | ||
|
|
df37166d42 | ||
|
|
25fa3a8f6a | ||
|
|
b11507c5e6 | ||
|
|
f06d02489f | ||
|
|
c203af2f71 | ||
|
|
c715155a70 | ||
|
|
8163133294 | ||
|
|
765be5dab4 | ||
|
|
7c1523389d | ||
|
|
7a2b1ba166 | ||
|
|
45b4dcfcd0 | ||
|
|
3b2e535566 | ||
|
|
db556d13a3 | ||
|
|
a987063c68 | ||
|
|
4ce30ef899 | ||
|
|
d988282d98 | ||
|
|
695fdf7ceb |
@@ -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.
|
||||
|
||||
|
||||

|
||||
@@ -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
|
||||
|
||||
@@ -68,6 +71,9 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
|
||||
|
||||

|
||||
|
||||
> ClipInterrogator
|
||||
|
||||
[add clip-interrogator](https://github.com/pharmapsychotic/clip-interrogator)
|
||||
|
||||
### Layers
|
||||
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
|
||||
@@ -164,9 +170,7 @@ v0.8.0 🚀🚗🚚🏃 LaMaInpainting
|
||||
|
||||
[Download lama](https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt), move to : models/lama
|
||||
|
||||
<!-- ### Workflow
|
||||
[Workflow](./workflow.md) -->
|
||||
|
||||
[Download Salesforce\blip-image-captioning-base](https://huggingface.co/Salesforce/blip-image-captioning-base), move to : models/clip_interrogator/Salesforce/blip-image-captioning-base
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
+81
-12
@@ -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
|
||||
@@ -446,6 +512,7 @@ from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
|
||||
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.Utils import AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,GetImageSize_,MultiplicationNode
|
||||
from .nodes.Lama import LaMaInpainting
|
||||
from .nodes.ClipInterrogator import ClipInterrogator
|
||||
|
||||
# 要导出的所有节点及其名称的字典
|
||||
# 注意:名称应全局唯一
|
||||
@@ -453,7 +520,9 @@ NODE_CLASS_MAPPINGS = {
|
||||
"AppInfo":AppInfo,
|
||||
"RandomPrompt":RandomPrompt,
|
||||
"PromptSlide":PromptSlide,
|
||||
"ClipInterrogator":ClipInterrogator,
|
||||
"NoiseImage":NoiseImage,
|
||||
"GradientImage":GradientImage,
|
||||
"TransparentImage":TransparentImage,
|
||||
"ResizeImageMixlab":ResizeImage,
|
||||
"LoadImagesFromPath":LoadImagesFromPath,
|
||||
|
||||
@@ -4774,6 +4774,7 @@
|
||||
"Color",
|
||||
"CombineMasks_",
|
||||
"EnhanceImage",
|
||||
"GradientImage",
|
||||
"FaceToMask",
|
||||
"FeatheredMask",
|
||||
"FloatingVideo",
|
||||
@@ -4785,6 +4786,7 @@
|
||||
"NewLayer",
|
||||
"RandomPrompt",
|
||||
"PromptSlide",
|
||||
"ClipInterrogator",
|
||||
"ScreenShare",
|
||||
"ShowLayer",
|
||||
"ShowTextForGPT",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
import folder_paths
|
||||
|
||||
from PIL import Image
|
||||
import comfy.utils
|
||||
import numpy as np
|
||||
import json
|
||||
import torch
|
||||
|
||||
from transformers import AutoProcessor, BlipForConditionalGeneration
|
||||
|
||||
from clip_interrogator import Config, Interrogator
|
||||
|
||||
def load_caption_model(model_path,config,t='blip-base'):
|
||||
dtype=torch.float16 if config.device == 'cuda' else torch.float32
|
||||
caption_model = BlipForConditionalGeneration.from_pretrained(model_path, torch_dtype=dtype)
|
||||
|
||||
caption_processor = AutoProcessor.from_pretrained(model_path)
|
||||
|
||||
caption_model.eval()
|
||||
if not config.caption_offload:
|
||||
caption_model = caption_model.to(config.device)
|
||||
|
||||
return (caption_model,caption_processor)
|
||||
|
||||
|
||||
|
||||
caption_model_path=os.path.join(folder_paths.models_dir, "clip_interrogator/Salesforce/blip-image-captioning-base")
|
||||
if not os.path.exists(caption_model_path):
|
||||
print(f"## clip_interrogator_model not found: {caption_model_path}, pls download from https://huggingface.co/Salesforce/blip-image-captioning-base")
|
||||
|
||||
cache_path=os.path.join(folder_paths.models_dir, "clip_interrogator")
|
||||
|
||||
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
# Convert PIL to Tensor
|
||||
def pil2tensor(image):
|
||||
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
|
||||
def image_analysis(ci,image):
|
||||
image = image.convert('RGB')
|
||||
image_features = ci.image_to_features(image)
|
||||
|
||||
top_mediums = ci.mediums.rank(image_features, 5)
|
||||
top_artists = ci.artists.rank(image_features, 5)
|
||||
top_movements = ci.movements.rank(image_features, 5)
|
||||
top_trendings = ci.trendings.rank(image_features, 5)
|
||||
top_flavors = ci.flavors.rank(image_features, 5)
|
||||
|
||||
medium_ranks = {medium: sim for medium, sim in zip(top_mediums, ci.similarities(image_features, top_mediums))}
|
||||
artist_ranks = {artist: sim for artist, sim in zip(top_artists, ci.similarities(image_features, top_artists))}
|
||||
movement_ranks = {movement: sim for movement, sim in zip(top_movements, ci.similarities(image_features, top_movements))}
|
||||
trending_ranks = {trending: sim for trending, sim in zip(top_trendings, ci.similarities(image_features, top_trendings))}
|
||||
flavor_ranks = {flavor: sim for flavor, sim in zip(top_flavors, ci.similarities(image_features, top_flavors))}
|
||||
|
||||
return medium_ranks, artist_ranks, movement_ranks, trending_ranks, flavor_ranks
|
||||
|
||||
def image_to_prompt(ci,image, mode):
|
||||
ci.config.chunk_size = 2048 if ci.config.clip_model_name == "ViT-L-14/openai" else 1024
|
||||
ci.config.flavor_intermediate_count = 2048 if ci.config.clip_model_name == "ViT-L-14/openai" else 1024
|
||||
image = image.convert('RGB')
|
||||
if mode == 'best':
|
||||
return ci.interrogate(image)
|
||||
elif mode == 'classic':
|
||||
return ci.interrogate_classic(image)
|
||||
elif mode == 'fast':
|
||||
return ci.interrogate_fast(image)
|
||||
elif mode == 'negative':
|
||||
return ci.interrogate_negative(image)
|
||||
|
||||
# image = Image.open(image_path).convert('RGB')
|
||||
# ci = Interrogator(Config(clip_model_name="ViT-L-14/openai"))
|
||||
# print(ci.interrogate(image))
|
||||
|
||||
|
||||
class ClipInterrogator:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"image": ("IMAGE",),
|
||||
"prompt_mode": (['fast','classic','best','negative'],),
|
||||
"image_analysis": (["off","on"],),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING","STRING",)
|
||||
RETURN_NAMES = ("prompt","analysis",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/prompt"
|
||||
|
||||
INPUT_IS_LIST = True
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
global ci
|
||||
ci = None
|
||||
def run(self,image,prompt_mode,image_analysis):
|
||||
global ci
|
||||
|
||||
prompt_mode=prompt_mode[0]
|
||||
analysis=image_analysis[0]
|
||||
|
||||
prompt_result=[]
|
||||
analysis_result=[]
|
||||
|
||||
# 进度条
|
||||
pbar = comfy.utils.ProgressBar(len(image)*(2 if analysis=='on' else 1))
|
||||
|
||||
if ci==None:
|
||||
config=Config(
|
||||
clip_model_name="ViT-L-14/openai",
|
||||
device="cuda" if torch.cuda.is_available() else "cpu",
|
||||
download_cache=True,
|
||||
clip_model_path=cache_path,
|
||||
cache_path=cache_path
|
||||
)
|
||||
config.apply_low_vram_defaults()
|
||||
|
||||
caption_model,caption_processor=load_caption_model(caption_model_path,config)
|
||||
|
||||
config.caption_model= caption_model
|
||||
config.caption_processor= caption_processor
|
||||
|
||||
ci = Interrogator(config)
|
||||
# else:
|
||||
# simple_lama.model.to("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
for i in range(len(image)):
|
||||
im=image[i]
|
||||
|
||||
im=tensor2pil(im)
|
||||
im=im.convert('RGB')
|
||||
|
||||
if analysis=='on':
|
||||
analysis_res=image_analysis(ci,im)
|
||||
analysis_result.append(json.dumps(analysis_res))
|
||||
pbar.update(1)
|
||||
|
||||
prompt=image_to_prompt(ci,im,prompt_mode)
|
||||
pbar.update(1)
|
||||
prompt_result.append(prompt)
|
||||
|
||||
|
||||
# result.save("inpainted.png")
|
||||
if ci.config.clip_offload and not ci.clip_offloaded:
|
||||
ci.clip_model = ci.clip_model.to('cpu')
|
||||
ci.clip_offloaded = True
|
||||
|
||||
if ci.config.caption_offload and not ci.caption_offloaded:
|
||||
ci.caption_model = ci.caption_model.to('cpu')
|
||||
ci.caption_offloaded = True
|
||||
|
||||
return {"ui":{"prompt": prompt_result,"analysis":analysis_result},"result": (prompt_result,analysis_result,)}
|
||||
+169
-18
@@ -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=[]
|
||||
@@ -467,26 +516,42 @@ def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option)
|
||||
return bg_image
|
||||
|
||||
|
||||
def resize_image(layer_image,scale_option,width,height):
|
||||
|
||||
def resize_image(layer_image, scale_option, width, height,color="white"):
|
||||
layer_image = layer_image.convert("RGB")
|
||||
original_width, original_height = layer_image.size
|
||||
|
||||
if scale_option == "height":
|
||||
# 按照高度比例缩放
|
||||
original_width, original_height = layer_image.size
|
||||
# Scale image based on height
|
||||
scale = height / original_height
|
||||
new_width = int(original_width * scale)
|
||||
layer_image = layer_image.resize((new_width, height))
|
||||
|
||||
elif scale_option == "width":
|
||||
# 按照宽度比例缩放
|
||||
original_width, original_height = layer_image.size
|
||||
# Scale image based on width
|
||||
scale = width / original_width
|
||||
new_height = int(original_height * scale)
|
||||
layer_image = layer_image.resize((width, new_height))
|
||||
|
||||
elif scale_option == "overall":
|
||||
# 整体缩放
|
||||
# Scale image overall
|
||||
layer_image = layer_image.resize((width, height))
|
||||
|
||||
elif scale_option == "center":
|
||||
# Scale image to minimum of width and height, center it, and fill extra area with black
|
||||
scale = min(width / original_width, height / original_height)
|
||||
new_width = int(original_width * scale)
|
||||
new_height = int(original_height * scale)
|
||||
resized_image = Image.new("RGB", (width, height), color=color)
|
||||
resized_image.paste(layer_image.resize((new_width, new_height)), ((width - new_width) // 2, (height - new_height) // 2))
|
||||
resized_image=resized_image.convert("RGB")
|
||||
return resized_image
|
||||
|
||||
return layer_image
|
||||
|
||||
|
||||
|
||||
|
||||
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
|
||||
# # Load Chinese font
|
||||
# font = ImageFont.truetype(font_path, font_size)
|
||||
@@ -863,7 +928,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
|
||||
@@ -885,20 +953,27 @@ class EnhanceImage:
|
||||
|
||||
CATEGORY = "♾️Mixlab/image"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
INPUT_IS_LIST = True
|
||||
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
|
||||
# 运行的函数
|
||||
def run(self,image,contrast):
|
||||
# print('EnhanceImage',image.shape)
|
||||
image=tensor2pil(image)
|
||||
|
||||
image=enhance_depth_map(image,contrast)
|
||||
# print('EnhanceImage',len(image),image[0].shape)
|
||||
contrast=contrast[0]
|
||||
res=[]
|
||||
for ims in image:
|
||||
for im in ims:
|
||||
|
||||
image=pil2tensor(image)
|
||||
image=tensor2pil(im)
|
||||
|
||||
image=enhance_depth_map(image,contrast)
|
||||
|
||||
image=pil2tensor(image)
|
||||
|
||||
res.append(image)
|
||||
|
||||
return (image,)
|
||||
return (res,)
|
||||
|
||||
|
||||
|
||||
@@ -1597,6 +1672,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):
|
||||
@@ -1692,13 +1841,14 @@ class ResizeImage:
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"scale_option": (["width","height",'overall'],),
|
||||
"scale_option": (["width","height",'overall','center'],),
|
||||
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"image": ("IMAGE",),
|
||||
"average_color": (["on",'off'],),
|
||||
"fill_color":("STRING",{"multiline": False,"default": "#FFFFFF","dynamicPrompts": False}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,12 +1862,13 @@ class ResizeImage:
|
||||
INPUT_IS_LIST = True
|
||||
OUTPUT_IS_LIST = (True,True,)
|
||||
|
||||
def run(self,width,height,scale_option,image=None,average_color=['on']):
|
||||
def run(self,width,height,scale_option,image=None,average_color=['on'],fill_color=["#FFFFFF"]):
|
||||
|
||||
w=width[0]
|
||||
h=height[0]
|
||||
scale_option=scale_option[0]
|
||||
average_color=average_color[0]
|
||||
fill_color=fill_color[0]
|
||||
|
||||
imgs=[]
|
||||
average_images=[]
|
||||
@@ -1734,7 +1885,7 @@ class ResizeImage:
|
||||
else:
|
||||
for im in image:
|
||||
im=tensor2pil(im)
|
||||
im=resize_image(im,scale_option,w,h)
|
||||
im=resize_image(im,scale_option,w,h,fill_color)
|
||||
im=im.convert('RGB')
|
||||
|
||||
a_im=get_average_color_image(im)
|
||||
|
||||
+39
-8
@@ -34,13 +34,13 @@ def create_temp_file(image):
|
||||
) = folder_paths.get_save_image_path('tmp', output_dir)
|
||||
|
||||
|
||||
image=tensor2pil(image)
|
||||
im=tensor2pil(image)
|
||||
|
||||
image_file = f"{filename}_{counter:05}.png"
|
||||
|
||||
image_path=os.path.join(full_output_folder, image_file)
|
||||
|
||||
image.save(image_path,compress_level=4)
|
||||
im.save(image_path,compress_level=4)
|
||||
|
||||
return [{
|
||||
"filename": image_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}),
|
||||
}
|
||||
|
||||
}
|
||||
@@ -450,16 +461,28 @@ class AppInfo:
|
||||
|
||||
CATEGORY = "♾️Mixlab"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
INPUT_IS_LIST = True
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
|
||||
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):
|
||||
name=name[0]
|
||||
im=image[0][0]
|
||||
# image [img,] img[batch,w,h,a] 列表里面是batch,
|
||||
|
||||
im=create_temp_file(image)
|
||||
input_ids=input_ids[0]
|
||||
output_ids=output_ids[0]
|
||||
description=description[0]
|
||||
version=version[0]
|
||||
share_prefix=share_prefix[0]
|
||||
link=link[0]
|
||||
category=category[0]
|
||||
|
||||
#TODO batch 的方式需要处理
|
||||
im=create_temp_file(im)
|
||||
|
||||
# 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 +523,7 @@ class SwitchByIndex:
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"flat": (['off',"on"],),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,13 +537,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]]
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ watchdog
|
||||
opencv-python-headless
|
||||
matplotlib
|
||||
openai
|
||||
simple-lama-inpainting
|
||||
simple-lama-inpainting
|
||||
clip-interrogator==0.6.0
|
||||
+518
-82
@@ -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 {
|
||||
@@ -254,7 +276,7 @@
|
||||
|
||||
.show_text {
|
||||
font-size: 14px;
|
||||
/* display: inline-block; */
|
||||
user-select: text;
|
||||
margin: 8px;
|
||||
padding: 32px;
|
||||
min-width: 200px;
|
||||
@@ -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 =
|
||||
@@ -346,6 +399,47 @@
|
||||
|
||||
}
|
||||
|
||||
async function getQueue(clientId) {
|
||||
try {
|
||||
|
||||
const res = await fetch(`${get_url()}/queue`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
// Running action uses a different endpoint for cancelling
|
||||
Running: Array.from(data.queue_running, prompt => {
|
||||
if (prompt[3].client_id === clientId) {
|
||||
let prompt_id = prompt[1];
|
||||
return {
|
||||
prompt_id,
|
||||
remove: () => interrupt(),
|
||||
}
|
||||
}
|
||||
}),
|
||||
Pending: data.queue_pending.map((prompt) => ({ prompt })),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { Running: [], Pending: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function interrupt() {
|
||||
try {
|
||||
await fetch(`${get_url()}/interrupt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
function randomSeed(seed, data) {
|
||||
for (const id in data) {
|
||||
@@ -354,7 +448,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 +461,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 +496,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 +597,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 +927,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) => {
|
||||
@@ -854,9 +980,20 @@
|
||||
|
||||
// Create an input field for the image name
|
||||
const textInput = document.createElement("textarea");
|
||||
// uploadImageInput.type = "text";
|
||||
// textInput.className=;
|
||||
textInput.value = data.inputs.text;
|
||||
// uploadImageInput.type = "text";
|
||||
let json = localStorage.getItem(`t_${data.id}`)
|
||||
try {
|
||||
const { value, height } = JSON.parse(json);
|
||||
textInput.value = value;
|
||||
textInput.style.height = height;
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
uploadContainer.appendChild(textInput);
|
||||
// autoResize(textInput);
|
||||
|
||||
function autoResize(textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
@@ -867,6 +1004,11 @@
|
||||
// console.log(textInput.value)
|
||||
autoResize(textInput);
|
||||
window._appData.data[data.id].inputs.text = textInput.value;
|
||||
|
||||
localStorage.setItem(`t_${data.id}`, JSON.stringify({
|
||||
value: textInput.value,
|
||||
height: textInput.style.height
|
||||
}));
|
||||
})
|
||||
|
||||
// Append the upload container to the main container
|
||||
@@ -898,14 +1040,133 @@
|
||||
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)
|
||||
try {
|
||||
let i=parseFloat(localStorage.getItem(`_slider_${targetId}`))
|
||||
if(!!i) value = i
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
// 输入div
|
||||
let inputDiv = document.createElement('div');
|
||||
@@ -926,23 +1187,29 @@
|
||||
label.setAttribute('data-content', value);
|
||||
|
||||
if (keywords && keywords[0]) {
|
||||
label.innerHTML = ``
|
||||
// label.innerHTML = ``
|
||||
// 有备选的关键词
|
||||
let selectTag = createSelect(Array.from([labelText, ...keywords], k => {
|
||||
|
||||
let defaultValue = (targetId ? localStorage.getItem(`_slide_${targetId}`) : '') || keywords[0];
|
||||
|
||||
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;`
|
||||
}), defaultValue);
|
||||
|
||||
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;
|
||||
// label.querySelector('.label').innerText = selectTag.value
|
||||
// console.log(window._appData.data[targetId].inputs.prompt_keyword,selectTag.value)
|
||||
|
||||
targetId ? localStorage.setItem(`_slide_${targetId}`, selectTag.value) : ''
|
||||
})
|
||||
label.appendChild(selectTag);
|
||||
}
|
||||
@@ -955,9 +1222,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') {
|
||||
@@ -979,6 +1244,8 @@
|
||||
label.setAttribute('data-content', value);
|
||||
// 在这里可以执行其他操作,根据需要进行相应的处理
|
||||
callback && callback(value)
|
||||
|
||||
localStorage.setItem(`_slider_${targetId}`, value)
|
||||
});
|
||||
|
||||
// 返回容器元素
|
||||
@@ -996,11 +1263,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 +1289,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 +1348,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 +1396,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,44 +1499,112 @@
|
||||
},
|
||||
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`)) {
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
if (val && type == "images" && output.querySelector(`#output_${id} img`)) {
|
||||
let imgDiv = output.querySelector(`#output_${id}`)
|
||||
imgDiv.style.display = 'none';
|
||||
|
||||
// 清空
|
||||
// Array.from(imgDiv.parentElement.querySelectorAll('.output_images'), im => im.remove());
|
||||
|
||||
for (const v of val) {
|
||||
let im = await createImage(v);
|
||||
|
||||
// 构建新的
|
||||
let a = document.createElement('a');
|
||||
a.className = `${imgDiv.id} output_images`
|
||||
a.setAttribute('data-pswp-width', im.naturalWidth);
|
||||
a.setAttribute('data-pswp-height', im.naturalHeight);
|
||||
a.setAttribute('target', "_blank");
|
||||
a.setAttribute('href', v);
|
||||
|
||||
let img = new Image();
|
||||
// img;
|
||||
img.src = v;
|
||||
a.appendChild(img)
|
||||
// imgDiv.parentElement.appendChild(a);
|
||||
imgDiv.parentElement.insertBefore(a, imgDiv.parentElement.firstChild);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
},
|
||||
submitButton: {
|
||||
element: submitButton,
|
||||
update: function (callback) {
|
||||
update: function (runFn, cancelFn) {
|
||||
submitButton.addEventListener('dblclick', (e) => {
|
||||
e.preventDefault()
|
||||
submitButton.classList.remove('disabled');
|
||||
});
|
||||
submitButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (submitButton.classList.contains('data-click')) {
|
||||
return
|
||||
} else {
|
||||
submitButton.classList.add('data-click')
|
||||
setTimeout(() => submitButton.classList.remove('data-click'), 500)
|
||||
}
|
||||
|
||||
if (!submitButton.classList.contains('disabled')) {
|
||||
callback && callback();
|
||||
runFn && runFn();
|
||||
submitButton.classList.add('disabled');
|
||||
setTimeout(() => submitButton.classList.remove('disabled'), 500)
|
||||
submitButton.innerText = 'Cancel'
|
||||
} else {
|
||||
// 如果能取消
|
||||
let canCancel = cancelFn && cancelFn();
|
||||
if (canCancel) submitButton.classList.remove('disabled');
|
||||
if (canCancel) submitButton.innerText = 'Create';
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
running: () => {
|
||||
submitButton.innerText = 'Cancel';
|
||||
if (!submitButton.classList.contains('disabled')) submitButton.classList.add('disabled');
|
||||
},
|
||||
reset: () => {
|
||||
submitButton.innerText = 'Create';
|
||||
submitButton.classList.remove('disabled');
|
||||
submitButton.classList.remove('data-click');
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1272,7 +1636,6 @@
|
||||
Array.from(detail.querySelectorAll('.card'), c => c.classList.remove('selected'));
|
||||
div.className = 'upload_btn card selected'
|
||||
|
||||
|
||||
let { output, app } = jsonData;
|
||||
|
||||
window._appData = {
|
||||
@@ -1311,10 +1674,19 @@
|
||||
ui.status.update(appData ? 'READY' : '-');
|
||||
|
||||
// 添加提交按钮点击事件
|
||||
ui.submitButton.update(function () {
|
||||
// 在提交按钮点击时执行的逻辑
|
||||
queuePrompt(window._appData.data, window._appData.seed, api.clientId)
|
||||
});
|
||||
ui.submitButton.update(
|
||||
() => {
|
||||
// 在提交按钮点击时执行的逻辑
|
||||
queuePrompt(window._appData.data, window._appData.seed, api.clientId);
|
||||
}, () => {
|
||||
// 取消
|
||||
if (api.runningCancel) {
|
||||
api.runningCancel();
|
||||
api.runningCancel = null;
|
||||
return true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
const show = (src, id, type = "image") => {
|
||||
@@ -1336,12 +1708,13 @@
|
||||
console.log("progress", detail);
|
||||
try {
|
||||
ui.status.update(`${detail.value}/${detail.max}`);
|
||||
ui.submitButton.running()
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
api.addEventListener("executed", async ({ detail }) => {
|
||||
console.log("executed", detail)
|
||||
// if (!enabled) return;
|
||||
const images = detail?.output?.images;
|
||||
@@ -1349,9 +1722,14 @@
|
||||
const gifs = detail?.output?.gifs;
|
||||
|
||||
if (images) {
|
||||
// if (!images) return;
|
||||
const src = `${get_url()}/view?filename=${encodeURIComponent(images[0].filename)}&type=${images[0].type}&subfolder=${encodeURIComponent(images[0].subfolder)}&t=${+new Date()}`;
|
||||
show(src, detail.node, 'image');
|
||||
// if (!images) return;
|
||||
|
||||
let url = get_url();
|
||||
|
||||
show(Array.from(images, img => {
|
||||
return `${url}/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}&t=${+new Date()}`;
|
||||
}), detail.node, 'images');
|
||||
|
||||
} else if (text) {
|
||||
ui.output.update("text", Array.isArray(text) ? text[0] : text, detail.node)
|
||||
} else if (gifs && gifs[0]) {
|
||||
@@ -1364,22 +1742,77 @@
|
||||
|
||||
|
||||
try {
|
||||
ui.status.update(`executed_#${detail.node}`);
|
||||
ui.status.update(`executed_#${window._appData.data[detail.node]?.class_type}`);
|
||||
ui.submitButton.reset()
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
// console.log(Running, Pending);
|
||||
try {
|
||||
const { Running, Pending } = await getQueue(api.clientId);
|
||||
if (Running && Running[0]) {
|
||||
api.runningCancel = Running[0].remove;
|
||||
ui.submitButton.running()
|
||||
} else {
|
||||
api.runningCancel = null;
|
||||
}
|
||||
} catch (error) {
|
||||
api.runningCancel = null;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
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', async ({ detail }) => {
|
||||
console.log("execution_start", detail)
|
||||
try {
|
||||
ui.status.update(`execution_start`);
|
||||
ui.submitButton.running()
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
const { Running, Pending } = await getQueue(api.clientId);
|
||||
if (Running && Running[0]) {
|
||||
api.runningCancel = Running[0].remove;
|
||||
} else {
|
||||
api.runningCancel = null;
|
||||
}
|
||||
} catch (error) {
|
||||
api.runningCancel = null;
|
||||
}
|
||||
})
|
||||
|
||||
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 +1854,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 +1870,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 +1879,13 @@
|
||||
|
||||
createApp(window._appData);
|
||||
|
||||
|
||||
};
|
||||
|
||||
init_app()
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -196,6 +196,9 @@ app.registerExtension({
|
||||
}
|
||||
if (bg) {
|
||||
data.bg_image = await parseImage(bg)
|
||||
if (!data.bg_image.match('data:image/')) {
|
||||
delete data.bg_image
|
||||
}
|
||||
}
|
||||
|
||||
if (material) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.10.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -132,13 +132,15 @@ app.registerExtension({
|
||||
inp.click()
|
||||
inp.addEventListener('change', event => {
|
||||
// 获取选择的文件
|
||||
const file = event.target.files[0]
|
||||
const file = event.target.files[0];
|
||||
this.title=file.name.split('.')[0];
|
||||
|
||||
// console.log(file.name.split('.')[0])
|
||||
// 创建文件读取器
|
||||
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 +150,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 +187,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
@@ -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
@@ -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) {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
+5
File diff suppressed because one or more lines are too long
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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}
|
||||
Vendored
+3
File diff suppressed because one or more lines are too long
+2423
-705
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user