diff --git a/README.en.md b/README.en.md
index ab958db..0b34e38 100644
--- a/README.en.md
+++ b/README.en.md
@@ -31,6 +31,12 @@
## Changelog
+**v1.1.4**
+
+- Added the right-click menu to view checkpoints and lora information in all Loaders
+- Fixed `easy preSamplingNoiseIn`、`easy latentNoisy`、`east Unsampler` compatible with ComfyUI Revision>=2098 [0542088e] or later
+
+
**v1.1.3 (2024/4/4)**
- `easy ipadapterApply` Added **COMPOSITION** preset
@@ -38,7 +44,7 @@
- Added `easy promptLine`
- Added `easy promptReplace`
- Added `easy promptConcat`
-- `easy wildcards` 增加 **multiline_mode**属性
+- `easy wildcards` Added **multiline_mode**
**v1.1.2 (39c5ccf)**
@@ -320,4 +326,10 @@ Disclaimer: Opened source was not easy. I have a lot of respect for the contribu
[ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) - General Modpack 2
-[ComfyUI-ResAdapter](https://github.com/jiaxiangc/ComfyUI-ResAdapter) - Make model generation independent of training resolution
\ No newline at end of file
+[ComfyUI-ResAdapter](https://github.com/jiaxiangc/ComfyUI-ResAdapter) - Make model generation independent of training resolution
+
+[ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) - Style migration
+
+[ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) - Face migration
+
+[ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss🐍
\ No newline at end of file
diff --git a/README.md b/README.md
index 2a83530..572777b 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,11 @@
## 更新日志
+**v1.1.4 (2024/4/7)**
+
+- 增加 在Loaders上右键菜单可查看 checkpoints、lora 信息
+- 修复 `easy preSamplingNoiseIn`、`easy latentNoisy`、`east Unsampler` 以兼容ComfyUI Revision>=2098 [0542088e] 以上版本
+
**v1.1.3 (2024/4/4)**
- `easy ipadapterApply` 增加 **COMPOSITION** 预置项
@@ -338,3 +343,9 @@
[ComfyUI-Logic](https://github.com/theUpsider/ComfyUI-Logic) - ComfyUI逻辑运算
[ComfyUI-ResAdapter](https://github.com/jiaxiangc/ComfyUI-ResAdapter) - 让模型生成不受训练分辨率限制
+
+[ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) - 风格迁移
+
+[ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) - 人脸迁移
+
+[ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss 小蛇🐍脚本
diff --git a/py/api.py b/py/api.py
index 75ad2a9..263d19a 100644
--- a/py/api.py
+++ b/py/api.py
@@ -130,6 +130,41 @@ async def getModelsThumbnail(request):
loras_full.append(full_path)
return web.json_response(checkpoints_full + loras_full)
+@PromptServer.instance.routes.post("/easyuse/metadata/notes/{name}")
+async def save_notes(request):
+ name = request.match_info["name"]
+ pos = name.index("/")
+ type = name[0:pos]
+ name = name[pos+1:]
+
+ file_path = None
+ if type == "embeddings" or type == "loras":
+ name = name.lower()
+ files = folder_paths.get_filename_list(type)
+ for f in files:
+ lower_f = f.lower()
+ if lower_f == name:
+ file_path = folder_paths.get_full_path(type, f)
+ else:
+ n = os.path.splitext(f)[0].lower()
+ if n == name:
+ file_path = folder_paths.get_full_path(type, f)
+
+ if file_path is not None:
+ break
+ else:
+ file_path = folder_paths.get_full_path(
+ type, name)
+ if not file_path:
+ return web.Response(status=404)
+
+ file_no_ext = os.path.splitext(file_path)[0]
+ info_file = file_no_ext + ".txt"
+ with open(info_file, "w") as f:
+ f.write(await request.text())
+
+ return web.Response(status=200)
+
@PromptServer.instance.routes.get("/easyuse/metadata/{name}")
async def load_metadata(request):
name = request.match_info["name"]
diff --git a/py/easyNodes.py b/py/easyNodes.py
index 35ccd6a..9c2d81a 100644
--- a/py/easyNodes.py
+++ b/py/easyNodes.py
@@ -1,7 +1,7 @@
import sys, os, re, json, time, math
import torch
import folder_paths
-import comfy.utils, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management
+import comfy.utils, comfy.sample, comfy.sampler_helpers, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management
from comfy.sd import CLIP, VAE
from comfy.model_patcher import ModelPatcher
from comfy_extras.chainner_models import model_loading
@@ -594,10 +594,9 @@ class latentNoisy:
device = comfy.model_management.get_torch_device()
end_at_step = min(steps, end_at_step)
start_at_step = min(start_at_step, end_at_step)
- real_model = None
comfy.model_management.load_model_gpu(model)
- real_model = model.model
- sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name,
+ model_patcher = comfy.model_patcher.ModelPatcher(model.model, load_device=device, offload_device=comfy.model_management.unet_offload_device())
+ sampler = comfy.samplers.KSampler(model_patcher, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sampler.sigmas
sigma = sigmas[start_at_step] - sigmas[end_at_step]
@@ -2903,8 +2902,8 @@ class samplerSettingsNoiseIn:
device = comfy.model_management.get_torch_device()
comfy.model_management.load_model_gpu(model)
- real_model = model.model
- sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name,
+ model_patcher = comfy.model_patcher.ModelPatcher(model.model, load_device=device, offload_device=comfy.model_management.unet_offload_device())
+ sampler = comfy.samplers.KSampler(model_patcher, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sampler.sigmas
sigma = sigmas[start_at_step] - sigmas[end_at_step]
@@ -3483,6 +3482,7 @@ class samplerFull(LayerDiffuse):
"vae": ("VAE",),
"clip": ("CLIP",),
"xyPlot": ("XYPLOT",),
+ "image": ("IMAGE",),
},
"hidden":
{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
@@ -4407,23 +4407,23 @@ class unsampler:
if "noise_mask" in latent:
noise_mask = comfy.sample.prepare_mask(latent["noise_mask"], noise.shape, device)
- real_model = None
- real_model = model.model
noise = noise.to(device)
latent_image = latent_image.to(device)
- positive = comfy.sample.convert_cond(positive)
- negative = comfy.sample.convert_cond(negative)
+ _positive = comfy.sampler_helpers.convert_cond(positive)
+ _negative = comfy.sampler_helpers.convert_cond(negative)
+ models, inference_memory = comfy.sampler_helpers.get_additional_models({"positive": _positive, "negative": _negative}, model.model_dtype())
- models, inference_memory = comfy.sample.get_additional_models(positive, negative, model.model_dtype())
comfy.model_management.load_models_gpu([model] + models, model.memory_required(noise.shape) + inference_memory)
- sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name,
+ model_patcher = comfy.model_patcher.ModelPatcher(model.model, load_device=device, offload_device=comfy.model_management.unet_offload_device())
+
+ sampler = comfy.samplers.KSampler(model_patcher, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
- sigmas = sigmas = sampler.sigmas.flip(0) + 0.0001
+ sigmas = sampler.sigmas.flip(0) + 0.0001
pbar = comfy.utils.ProgressBar(steps)
diff --git a/py/libs/utils.py b/py/libs/utils.py
index 40cd265..6e8cd3f 100644
--- a/py/libs/utils.py
+++ b/py/libs/utils.py
@@ -125,6 +125,7 @@ use_mirror = False
def get_local_filepath(url, dirname, local_file_name=None):
"""Get local file path when is already downloaded or download it"""
import os
+ from server import PromptServer
from urllib.parse import urlparse
from torch.hub import download_url_to_file
global use_mirror
@@ -139,17 +140,19 @@ def get_local_filepath(url, dirname, local_file_name=None):
if use_mirror:
url = url.replace('huggingface.co', 'hf-mirror.com')
print(f'downloading {url} to {destination}')
+ PromptServer.instance.send_sync("easyuse-toast", {'content': f'Downloading model to {destination}, please wait...', 'duration': 10000})
download_url_to_file(url, destination)
except Exception as e:
- args = e.args[0] if e.args is not None and len(e.args) > 0 else None
- if isinstance(args, TimeoutError):
- print(f'无法从 {url} 下载,正在尝试使用镜像地址...')
- use_mirror = True
- url = url.replace('huggingface.co', 'hf-mirror.com')
- try:
- download_url_to_file(url, destination)
- except Exception as err:
- raise Exception(f'无法从 {url} 下载,错误信息:{str(err.args[0])}')
+ use_mirror = True
+ url = url.replace('huggingface.co', 'hf-mirror.com')
+ print(f'无法从huggingface下载,正在尝试从 {url} 下载...')
+ PromptServer.instance.send_sync("easyuse-toast", {'content': f'无法连接huggingface,正在尝试从 {url} 下载...', 'duration': 10000})
+ try:
+ download_url_to_file(url, destination)
+ except Exception as err:
+ PromptServer.instance.send_sync("easyuse-toast",
+ {'content': f'无法从 {url} 下载模型'}, type='error')
+ raise Exception(f'无法从 {url} 下载,错误信息:{str(err.args[0])}')
return destination
def to_lora_patch_dict(state_dict: dict) -> dict:
diff --git a/web/css/index.css b/web/css/index.css
index 06e59db..c5756bc 100644
--- a/web/css/index.css
+++ b/web/css/index.css
@@ -3,4 +3,5 @@
@import "selector.css";
@import "groupmap.css";
@import "contextmenu.css";
-@import "modelinfo.css";
\ No newline at end of file
+@import "modelinfo.css";
+@import "toast.css";
\ No newline at end of file
diff --git a/web/css/modelinfo.css b/web/css/modelinfo.css
index e8158f5..370c21f 100644
--- a/web/css/modelinfo.css
+++ b/web/css/modelinfo.css
@@ -46,8 +46,9 @@
padding: 0;
}
.easyuse-model-tag {
- background-color: rgb(128, 213, 247);
- color: #000;
+ background-color: var(--comfy-input-bg);
+ border: 2px solid var(--border-color);
+ color: var(--input-text);
display: flex;
align-items: center;
gap: 5px;
@@ -66,7 +67,7 @@
text-align: center;
}
.easyuse-model-tag:hover {
- outline: 2px solid var(--theme-color-light);
+ border: 2px solid var(--theme-color-light);
}
.easyuse-model-tag p {
margin: 0;
@@ -79,6 +80,7 @@
position: relative;
min-width: 20px;
overflow: hidden;
+ color: #fff;
}
.easyuse-model-metadata .comfy-modal-content {
@@ -229,6 +231,30 @@
padding: 5px 10px 5px 10px;
color: var(--input-text);
}
+.easyuse-model-detail-textarea{
+ border-top:1px solid var(--border-color);
+ padding:10px;
+ height:100px;
+ overflow-y: auto;
+ font-size: 12px;
+}
+.easyuse-model-detail-textarea textarea{
+ width:100%;
+ height:100%;
+ border:0;
+ background-color:transparent;
+ color: var(--input-text);
+}
+.easyuse-model-detail-textarea textarea::placeholder{
+ color:var(--descrip-text);
+}
+.easyuse-model-detail-textarea.empty{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: var(--descrip-text);
+}
+
.easyuse-model-notes {
background-color: rgba(0, 0, 0, 0.25);
padding: 5px;
diff --git a/web/css/theme.css b/web/css/theme.css
index 78b92f3..aca7095 100644
--- a/web/css/theme.css
+++ b/web/css/theme.css
@@ -1,5 +1,8 @@
:root {
--theme-color:#3f3eed;
--theme-color-light:#006691;
+ --success-color: #52c41a;
+ --error-color: #ff4d4f;
+ --warning-color: #faad14;
--font-family: Inter, -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
}
\ No newline at end of file
diff --git a/web/css/toast.css b/web/css/toast.css
new file mode 100644
index 0000000..b0166fb
--- /dev/null
+++ b/web/css/toast.css
@@ -0,0 +1,110 @@
+.easyuse-toast-container{
+ position: fixed;
+ z-index: 99999;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: start;
+ padding:10px 0;
+}
+.easyuse-toast-container > div {
+ position: relative;
+ height: fit-content;
+ padding: 4px;
+ margin-top: -100px; /* re-set by JS */
+ opacity: 0;
+ transition: all 0.33s ease-in-out;
+ z-index: 3;
+}
+
+.easyuse-toast-container > div:last-child {
+ z-index: 2;
+}
+
+.easyuse-toast-container > div:not(.-show) {
+ z-index: 1;
+}
+
+.easyuse-toast-container > div.-show {
+ opacity: 1;
+ margin-top: 0px !important;
+}
+
+.easyuse-toast-container > div.-show {
+ opacity: 1;
+ transform: translateY(0%);
+}
+
+.easyuse-toast-container > div > div {
+ position: relative;
+ background: var(--comfy-menu-bg);
+ color: var(--input-text);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ height: fit-content;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.88);
+ padding: 9px 12px;
+ border-radius: 8px;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ pointer-events: all;
+}
+
+.easyuse-toast-container > div > div > span {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+
+.easyuse-toast-container > div > div > span svg {
+ width: 16px;
+ height: auto;
+ margin-right: 8px;
+}
+
+.easyuse-toast-container > div > div > span svg[data-icon=info-circle]{
+ fill: var(--theme-color-light);
+}
+.easyuse-toast-container > div > div > span svg[data-icon=check-circle]{
+ fill: var(--success-color);
+}
+.easyuse-toast-container > div > div > span svg[data-icon=close-circle]{
+ fill: var(--error-color);
+}
+.easyuse-toast-container > div > div > span svg[data-icon=exclamation-circle]{
+ fill: var(--warning-color);
+}
+/*rotate animation*/
+@keyframes rotate {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+.easyuse-toast-container > div > div > span svg[data-icon=loading]{
+ fill: var(--theme-color);
+ animation: rotate 1s linear infinite;
+}
+
+.easyuse-toast-container a {
+ cursor: pointer;
+ text-decoration: underline;
+ color: var(--theme-color-light);
+ margin-left: 4px;
+ display: inline-block;
+ line-height: 1;
+}
+
+.easyuse-toast-container a:hover {
+ color: var(--theme-color-light);
+ text-decoration: none;
+}
\ No newline at end of file
diff --git a/web/js/common/i18n.js b/web/js/common/i18n.js
index 6542b04..b1defc6 100644
--- a/web/js/common/i18n.js
+++ b/web/js/common/i18n.js
@@ -9,9 +9,21 @@ const zhCN = {
// ModelInfo
"Updated At:": "最近更新:",
"Created At:": "首次发布:",
+ "✏️ Edit": "✏️ 编辑",
+ "💾 Save": "💾 保存",
+ "No notes": "当前还没有备注内容",
+ "Saving Notes...": "正在保存备注...",
+ "Type your notes here":"在这里输入备注内容",
+ "Notes": "备注",
+ "Type": "类型",
+ "Trained Words": "训练词",
+ "BaseModel": "基础算法",
"Details": "详情",
"Download": "下载量",
"Source": "来源",
+ "Saving Preview...": "正在保存预览图...",
+ "Saving Succeed":"保存成功",
+ "Saving Failed":"保存失败",
// GroupMap
"Groups Map (EasyUse)": "管理组 (EasyUse)",
"Always": "启用中",
diff --git a/web/js/common/model.js b/web/js/common/model.js
index a80864e..a37ce92 100644
--- a/web/js/common/model.js
+++ b/web/js/common/model.js
@@ -2,7 +2,7 @@ import { $el, ComfyDialog } from "/scripts/ui.js";
import { api } from "/scripts/api.js";
import {formatTime} from './utils.js';
import {$t} from "./i18n.js";
-
+import {toast} from "./toast.js";
class MetadataDialog extends ComfyDialog {
constructor() {
@@ -93,115 +93,47 @@ export class ModelInfoDialog extends ComfyDialog {
return btns;
}
- getNoteInfo() {
- function parseNote() {
- if (!this.customNotes) return [];
+ parseNote() {
+ if (!this.customNotes) return [];
- let notes = [];
- // Extract links from notes
- const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
- let end = 0;
- let m;
- do {
- m = r.exec(this.customNotes);
- let pos;
- let fin = 0;
- if (m) {
- pos = m.index;
- fin = m.index + m[0].length;
- } else {
- pos = this.customNotes.length;
- }
+ let notes = [];
+ // Extract links from notes
+ const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
+ let end = 0;
+ let m;
+ do {
+ m = r.exec(this.customNotes);
+ let pos;
+ let fin = 0;
+ if (m) {
+ pos = m.index;
+ fin = m.index + m[0].length;
+ } else {
+ pos = this.customNotes.length;
+ }
- let pre = this.customNotes.substring(end, pos);
- if (pre) {
- pre = pre.replaceAll("\n", "
");
- notes.push(
- $el("span", {
- innerHTML: pre,
- })
- );
- }
- if (m) {
- notes.push(
- $el("a", {
- href: m[0],
- textContent: m[0],
- target: "_blank",
- })
- );
- }
+ let pre = this.customNotes.substring(end, pos);
+ if (pre) {
+ pre = pre.replaceAll("\n", "
");
+ notes.push(
+ $el("span", {
+ innerHTML: pre,
+ })
+ );
+ }
+ if (m) {
+ notes.push(
+ $el("a", {
+ href: m[0],
+ textContent: m[0],
+ target: "_blank",
+ })
+ );
+ }
- end = fin;
- } while (m);
- return notes;
- }
-
- let textarea;
- let notesContainer;
- const editText = "✏️ Edit";
- const edit = $el("a", {
- textContent: editText,
- href: "#",
- style: {
- float: "right",
- color: "greenyellow",
- textDecoration: "none",
- },
- onclick: async (e) => {
- e.preventDefault();
-
- if (textarea) {
- this.customNotes = textarea.value;
-
- const resp = await api.fetchApi(
- "/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
- {
- method: "POST",
- body: this.customNotes,
- }
- );
-
- if (resp.status !== 200) {
- console.error(resp);
- alert(`Error saving notes (${req.status}) ${req.statusText}`);
- return;
- }
-
- e.target.textContent = editText;
- textarea.remove();
- textarea = null;
-
- notesContainer.replaceChildren(...parseNote.call(this));
- } else {
- e.target.textContent = "💾 Save";
- textarea = $el("textarea", {
- style: {
- width: "100%",
- minWidth: "200px",
- minHeight: "50px",
- },
- textContent: this.customNotes,
- });
- e.target.after(textarea);
- notesContainer.replaceChildren();
- textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px";
- }
- },
- });
-
- notesContainer = $el("div.easyuse-model-notes", parseNote.call(this));
- return $el(
- "div",
- {
- style: { display: "contents" },
- },
- [edit, notesContainer]
- );
- }
-
- addInfo() {
- // this.addInfoEntry("Notes", this.getNoteInfo());
+ end = fin;
+ } while (m);
+ return notes;
}
addInfoEntry(name, value) {
@@ -250,6 +182,7 @@ export class ModelInfoDialog extends ComfyDialog {
return promise
.then((info) => {
+ this.imgWrapper.style.display = 'block'
// 变更标题信息
let header = this.element.querySelector('.easyuse-model-header')
if(header){
@@ -262,10 +195,90 @@ export class ModelInfoDialog extends ComfyDialog {
)
}
// 替换内容
+ let textarea = null
+ let notes = this.parseNote.call(this)
+ let editText = $t("✏️ Edit")
+ console.log(notes)
+ let textarea_div = $el("div.easyuse-model-detail-textarea",[
+ $el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}),
+ ])
+ if(!notes || notes.length == 0) textarea_div.classList.add('empty')
+ else textarea_div.classList.remove('empty')
this.info.replaceChildren(
+ $el("div.easyuse-model-detail",[
+ $el("div.easyuse-model-detail-head.flex-b",[
+ $el('span',$t("Notes")),
+ $el("a", {
+ textContent: editText,
+ href: "#",
+ style: {
+ fontSize: "12px",
+ float: "right",
+ color: "var(--warning-color)",
+ textDecoration: "none",
+ },
+ onclick: async (e) => {
+ e.preventDefault();
+
+ if (textarea) {
+ if(textarea.value != this.customNotes){
+ toast.showLoading($t('Saving Notes...'))
+ this.customNotes = textarea.value;
+ const resp = await api.fetchApi(
+ "/easyuse/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
+ {
+ method: "POST",
+ body: this.customNotes,
+ }
+ );
+ toast.hideLoading()
+ if (resp.status !== 200) {
+ toast.error($t('Saving Failed'))
+ console.error(resp);
+ alert(`Error saving notes (${resp.status}) ${resp.statusText}`);
+ return;
+ }
+ toast.success($t('Saving Succeed'))
+ notes = this.parseNote.call(this)
+ console.log(notes)
+ textarea_div.replaceChildren($el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}));
+ if(textarea.value) textarea_div.classList.remove('empty')
+ else textarea_div.classList.add('empty')
+ }else {
+ textarea_div.replaceChildren($el("p",{textContent:$t('No notes')}));
+ textarea_div.classList.add('empty')
+ }
+ e.target.textContent = editText;
+ textarea.remove();
+ textarea = null;
+
+ } else {
+ e.target.textContent = "💾 Save";
+ textarea = $el("textarea", {
+ placeholder: $t("Type your notes here"),
+ style: {
+ width: "100%",
+ minWidth: "200px",
+ minHeight: "50px",
+ height:"100px"
+ },
+ textContent: this.customNotes,
+ });
+ textarea_div.replaceChildren(textarea);
+ textarea.focus()
+ }
+ }
+ })
+ ]),
+ textarea_div
+ ]),
$el("div.easyuse-model-detail",[
$el("div.easyuse-model-detail-head",{textContent:$t("Details")}),
$el("div.easyuse-model-detail-body",[
+ $el("div.easyuse-model-detail-item",[
+ $el("div.easyuse-model-detail-item-label",{textContent:$t("Type")}),
+ $el("div.easyuse-model-detail-item-value",{textContent:info.model.type}),
+ ]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("BaseModel")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.baseModel}),
@@ -274,6 +287,10 @@ export class ModelInfoDialog extends ComfyDialog {
$el("div.easyuse-model-detail-item-label",{textContent:$t("Download")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.stats?.downloadCount || 0}),
]),
+ $el("div.easyuse-model-detail-item",[
+ $el("div.easyuse-model-detail-item-label",{textContent:$t("Trained Words")}),
+ $el("div.easyuse-model-detail-item-value",{textContent:info?.trainedWords.join(',') || '-'}),
+ ]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Source")}),
$el("div.easyuse-model-detail-item-value",[
@@ -301,6 +318,7 @@ export class ModelInfoDialog extends ComfyDialog {
if (info.images?.length) {
this.imgCurrent = 0
+ this.isSaving = false
info.images.map(cate=>
cate.url &&
this.imgList.appendChild(
@@ -310,6 +328,9 @@ export class ModelInfoDialog extends ComfyDialog {
$el("div.save", {
textContent: "Save as preview",
onclick: async () => {
+ if(this.isSaving) return
+ this.isSaving = true
+ toast.showLoading($t('Saving Preview...'))
// Convert the preview to a blob
const blob = await (await fetch(cate.url)).blob();
@@ -326,6 +347,9 @@ export class ModelInfoDialog extends ComfyDialog {
});
if (resp.status !== 200) {
+ this.isSaving = false
+ toast.error($t('Saving Failed'))
+ toast.hideLoading()
console.error(resp);
alert(`Error saving preview (${req.status}) ${req.statusText}`);
return;
@@ -341,7 +365,11 @@ export class ModelInfoDialog extends ComfyDialog {
headers: {
"content-type": "application/json",
},
+ }).then(_=>{
+ toast.success($t('Saving Succeed'))
+ toast.hideLoading()
});
+ this.isSaving = false
app.refreshComboInNodes();
},
})
@@ -357,8 +385,12 @@ export class ModelInfoDialog extends ComfyDialog {
// 添加按钮
this.slideLeftButton = $el("button.left",{
parent: this.imgWrapper,
+ style:{
+ display:info.images.length <= 2 ? 'none' : 'block'
+ },
innerHTML:``,
onclick: ()=>{
+ if(info.images.length <= 2) return
_this.imgList.classList.remove("no-transition")
if(_this.imgCurrent == 0){
_this.imgCurrent = (info.images.length/2)-1
@@ -382,9 +414,14 @@ export class ModelInfoDialog extends ComfyDialog {
})
this.slideRightButton = $el("button.right",{
parent: this.imgWrapper,
+ style:{
+ display:info.images.length <= 2 ? 'none' : 'block'
+ },
innerHTML:``,
onclick: ()=>{
+ if(info.images.length <= 2) return
_this.imgList.classList.remove("no-transition")
+
if( _this.imgCurrent >= (info.images.length/2)-1){
_this.imgCurrent = 0
const max = info.images.length/2
@@ -410,31 +447,32 @@ export class ModelInfoDialog extends ComfyDialog {
}
+ if(info.description){
+ $el("div", {
+ parent: this.content,
+ innerHTML: info.description,
+ style: {
+ marginTop: "10px",
+ },
+ });
+ }
+
return info;
})
.catch((err) => {
+ this.imgWrapper.style.display = 'none'
content.textContent = "⚠️ " + err.message;
- });
+ })
+ .finally(_=>{
+ })
}
}
export class CheckpointInfoDialog extends ModelInfoDialog {
async addInfo() {
- super.addInfo();
- const info = await this.addCivitaiInfo();
- if (info) {
- // this.addInfoEntry("Base Model", info.baseModel || "⚠️ Unknown");
- //
- // $el("div", {
- // parent: this.content,
- // innerHTML: info.description,
- // style: {
- // maxHeight: "250px",
- // overflow: "auto",
- // },
- // });
- }
+ // super.addInfo();
+ await this.addCivitaiInfo();
}
}
@@ -545,45 +583,45 @@ export class LoraInfoDialog extends ModelInfoDialog {
}
async addInfo() {
- this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
- this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
- this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
+ // this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
+ // this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
+ // this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
+ //
+ // this.addInfoEntry(
+ // "Resolution",
+ // $el(
+ // "select",
+ // this.getResolutions().map((r) => $el("option", { textContent: r }))
+ // )
+ // );
- this.addInfoEntry(
- "Resolution",
- $el(
- "select",
- this.getResolutions().map((r) => $el("option", { textContent: r }))
- )
- );
-
- super.addInfo();
+ // super.addInfo();
const p = this.addCivitaiInfo();
this.addTags();
const info = await p;
if (info) {
- $el(
- "p",
- {
- parent: this.content,
- textContent: "Trained Words: ",
- },
- [
- $el("pre", {
- textContent: info.trainedWords.join(", "),
- style: {
- whiteSpace: "pre-wrap",
- margin: "10px 0",
- background: "#222",
- padding: "5px",
- borderRadius: "5px",
- maxHeight: "250px",
- overflow: "auto",
- },
- }),
- ]
- );
+ // $el(
+ // "p",
+ // {
+ // parent: this.content,
+ // textContent: "Trained Words: ",
+ // },
+ // [
+ // $el("pre", {
+ // textContent: info.trainedWords.join(", "),
+ // style: {
+ // whiteSpace: "pre-wrap",
+ // margin: "10px 0",
+ // background: "#222",
+ // padding: "5px",
+ // borderRadius: "5px",
+ // maxHeight: "250px",
+ // overflow: "auto",
+ // },
+ // }),
+ // ]
+ // );
$el("div", {
parent: this.content,
innerHTML: info.description,
diff --git a/web/js/common/toast.js b/web/js/common/toast.js
new file mode 100644
index 0000000..ca9346a
--- /dev/null
+++ b/web/js/common/toast.js
@@ -0,0 +1,118 @@
+import {sleep} from "./utils.js";
+
+class Toast{
+
+ constructor() {
+ this.info_icon = ``
+ this.success_icon = ``
+ this.error_icon = ``
+ this.warn_icon = ``
+ this.loading_icon = ``
+ }
+
+ async showToast(data){
+ let container = document.querySelector(".easyuse-toast-container");
+ if (!container) {
+ container = document.createElement("div");
+ container.classList.add("easyuse-toast-container");
+ document.body.appendChild(container);
+ }
+ await this.hideToast(data.id);
+ const toastContainer = document.createElement("div");
+ const content = document.createElement("span");
+ content.innerHTML = data.content;
+ toastContainer.appendChild(content);
+ for (let a = 0; a < (data.actions || []).length; a++) {
+ const action = data.actions[a];
+ if (a > 0) {
+ const sep = document.createElement("span");
+ sep.innerHTML = " | ";
+ toastContainer.appendChild(sep);
+ }
+ const actionEl = document.createElement("a");
+ actionEl.innerText = action.label;
+ if (action.href) {
+ actionEl.target = "_blank";
+ actionEl.href = action.href;
+ }
+ if (action.callback) {
+ actionEl.onclick = (e) => {
+ return action.callback(e);
+ };
+ }
+ toastContainer.appendChild(actionEl);
+ }
+ const animContainer = document.createElement("div");
+ animContainer.setAttribute("toast-id", data.id);
+ animContainer.appendChild(toastContainer);
+ container.appendChild(animContainer);
+ await sleep(64);
+ animContainer.style.marginTop = `-${animContainer.offsetHeight}px`;
+ await sleep(64);
+ animContainer.classList.add("-show");
+ if (data.duration) {
+ await sleep(data.duration);
+ this.hideToast(data.id);
+ }
+ }
+ async hideToast(id) {
+ const msg = document.querySelector(`.easyuse-toast-container > [toast-id="${id}"]`);
+ if (msg === null || msg === void 0 ? void 0 : msg.classList.contains("-show")) {
+ msg.classList.remove("-show");
+ await sleep(750);
+ }
+ msg && msg.remove();
+ }
+ async clearAllMessages() {
+ let container = document.querySelector(".easyuse-toast-container");
+ container && (container.innerHTML = "");
+ }
+
+ async info(content, duration = 3000, actions = []) {
+ this.showToast({
+ id: `toast-info`,
+ content: `${this.info_icon} ${content}`,
+ duration,
+ actions
+ });
+ }
+ async success(content, duration = 3000, actions = []) {
+ this.showToast({
+ id: `toast-success`,
+ content: `${this.success_icon} ${content}`,
+ duration,
+ actions
+ });
+ }
+ async error(content, duration = 3000, actions = []) {
+ this.showToast({
+ id: `toast-error`,
+ content: `${this.error_icon} ${content}`,
+ duration,
+ actions
+ });
+ }
+ async warn(content, duration = 3000, actions = []) {
+ this.showToast({
+ id: `toast-warn`,
+ content: `${this.warn_icon} ${content}`,
+ duration,
+ actions
+ });
+ }
+ async showLoading(content, duration = 0, actions = []) {
+ this.showToast({
+ id: `toast-loading`,
+ content: `${this.loading_icon} ${content}`,
+ duration,
+ actions
+ });
+ }
+
+ async hideLoading() {
+ this.hideToast("toast-loading");
+ }
+
+}
+
+export const toast = new Toast();
diff --git a/web/js/common/utils.js b/web/js/common/utils.js
index 0537b8a..9baebae 100644
--- a/web/js/common/utils.js
+++ b/web/js/common/utils.js
@@ -1,3 +1,10 @@
+export function sleep(ms = 100, value) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(value);
+ }, ms);
+ });
+}
export function addPreconnect(href, crossorigin=false){
const preconnect = document.createElement("link");
preconnect.rel = 'preconnect'
@@ -5,7 +12,6 @@ export function addPreconnect(href, crossorigin=false){
if(crossorigin) preconnect.crossorigin = ''
document.head.appendChild(preconnect);
}
-
export function addCss(href, base=true) {
const link = document.createElement("link");
link.rel = "stylesheet";
diff --git a/web/js/easy/easy.js b/web/js/easy/easy.js
index f79482b..02c195e 100644
--- a/web/js/easy/easy.js
+++ b/web/js/easy/easy.js
@@ -1,9 +1,26 @@
+import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import {deepEqual,addCss} from "../common/utils.js";
import {$t} from '../common/i18n.js';
+import {toast} from "../common/toast.js";
addCss('css/index.css')
+api.addEventListener("easyuse-toast",event=>{
+ const content = event.detail.content
+ const type = event.detail.type
+ const duration = event.detail.duration
+ if(!type){
+ toast.info(content, duration)
+ }
+ else{
+ toast.showToast({
+ id: `toast-${type}`,
+ content: `${toast[type+"_icon"]} ${content}`,
+ duration: duration || 3000,
+ })
+ }
+})
app.registerExtension({
name: "comfy.easyUse",
init() {
diff --git a/web/js/easy/easyExtraMenu.js b/web/js/easy/easyExtraMenu.js
index c904c20..3d1f2ad 100644
--- a/web/js/easy/easyExtraMenu.js
+++ b/web/js/easy/easyExtraMenu.js
@@ -267,23 +267,23 @@ const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => {
callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include)
})
if(type == 'loaders'){
- // options.unshift({
- // content: $t("💎 View Lora Info..."),
- // callback: (value, options, e, menu, node) => {
- // const widget = node.widgets.find(cate=> cate.name == 'lora_name')
- // let name = widget.value;
- // if (!name || name == 'None') return
- // new LoraInfoDialog(name).show('loras', name);
- // }
- // })
- // options.unshift({
- // content: $t("💎 View Checkpoint Info..."),
- // callback: (value, options, e, menu, node) => {
- // let name = node.widgets[0].value;
- // if (!name || name == 'None') return
- // new CheckpointInfoDialog(name).show('checkpoints', name);
- // }
- // })
+ options.unshift({
+ content: $t("💎 View Lora Info..."),
+ callback: (value, options, e, menu, node) => {
+ const widget = node.widgets.find(cate=> cate.name == 'lora_name')
+ let name = widget.value;
+ if (!name || name == 'None') return
+ new LoraInfoDialog(name).show('loras', name);
+ }
+ })
+ options.unshift({
+ content: $t("💎 View Checkpoint Info..."),
+ callback: (value, options, e, menu, node) => {
+ let name = node.widgets[0].value;
+ if (!name || name == 'None') return
+ new CheckpointInfoDialog(name).show('checkpoints', name);
+ }
+ })
}
})
}