From 35f19b75fa495470a133fe18ccfb454f39e145be Mon Sep 17 00:00:00 2001 From: yolain Date: Sun, 7 Apr 2024 12:40:24 +0800 Subject: [PATCH] fix:latentNoisy & preSamplingNoiseIn & Unsampler --- README.en.md | 16 +- README.md | 11 ++ py/api.py | 35 ++++ py/easyNodes.py | 26 +-- py/libs/utils.py | 21 ++- web/css/index.css | 3 +- web/css/modelinfo.css | 32 +++- web/css/theme.css | 3 + web/css/toast.css | 110 +++++++++++ web/js/common/i18n.js | 12 ++ web/js/common/model.js | 348 +++++++++++++++++++---------------- web/js/common/toast.js | 118 ++++++++++++ web/js/common/utils.js | 8 +- web/js/easy/easy.js | 17 ++ web/js/easy/easyExtraMenu.js | 34 ++-- 15 files changed, 593 insertions(+), 201 deletions(-) create mode 100644 web/css/toast.css create mode 100644 web/js/common/toast.js 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); + } + }) } }) }