From 2ad46f68d0b549f8d631d62249923a0997d27635 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:38:08 +0300 Subject: [PATCH] Upgrading the RS Models Loader node --- Rayko_Models_Loader.py | 162 +++++++++------------ __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rayko_lora_widget.js | 299 ++++++++++++++++++++------------------- 5 files changed, 225 insertions(+), 242 deletions(-) diff --git a/Rayko_Models_Loader.py b/Rayko_Models_Loader.py index 9528ba6..c651017 100644 --- a/Rayko_Models_Loader.py +++ b/Rayko_Models_Loader.py @@ -24,9 +24,10 @@ import aiohttp CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PRESETS_DIR = os.path.join(CURRENT_DIR, "preset_models") +LORA_PRESETS_DIR = os.path.join(PRESETS_DIR, "loras") -if not os.path.exists(PRESETS_DIR): - os.makedirs(PRESETS_DIR) +os.makedirs(PRESETS_DIR, exist_ok=True) +os.makedirs(LORA_PRESETS_DIR, exist_ok=True) class RaykoModelsLoader: @classmethod @@ -36,13 +37,10 @@ class RaykoModelsLoader: vae_files = folder_paths.get_filename_list("vae") vae_files_with_pixel = vae_files + ["pixel_space"] - weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"] - clip_type_opts = [ "stable_diffusion", "stable_cascade", "sd3", "flux", "flux2", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "ltxv", "wan", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit", ] - device_opts = ["default", "cpu", "cuda"] required = { @@ -67,98 +65,51 @@ class RaykoModelsLoader: @classmethod def IS_CHANGED(cls, lora_data="[]", **kwargs): import hashlib - if lora_data is None: - lora_data = "[]" + if lora_data is None: lora_data = "[]" return hashlib.md5(lora_data.encode()).hexdigest() def load_models(self, unet_name, weight_dtype, use_clip2, clip_name, clip_name2, clip_type, clip_device, vae_name, lora_data="[]"): - print(f"\n[Rayko] === Start load_models ===") - print(f"[Rayko] lora_data RAW: {lora_data}") - print(f"[Rayko] use_clip2: {use_clip2}") - - if not lora_data: - lora_data = "[]" + if not lora_data: lora_data = "[]" wd = None if weight_dtype == "default" else weight_dtype model = UNETLoader().load_unet(unet_name=unet_name, weight_dtype=wd)[0] - print(f"[Rayko] UNET: {unet_name}") - dev = None if clip_device == "default" else clip_device if use_clip2 and clip_name2 and clip_name2 != "None": - print(f"[Rayko] Using DualCLIPLoader mode") - print(f"[Rayko] CLIP1: {clip_name}") - print(f"[Rayko] CLIP2: {clip_name2}") - print(f"[Rayko] CLIP type: {clip_type}") - clip = DualCLIPLoader().load_clip( - clip_name1=clip_name, - clip_name2=clip_name2, - type=clip_type, - device=dev - )[0] + clip = DualCLIPLoader().load_clip(clip_name1=clip_name, clip_name2=clip_name2, type=clip_type, device=dev)[0] else: - print(f"[Rayko] Using standard CLIPLoader mode") - print(f"[Rayko] CLIP: {clip_name}") - print(f"[Rayko] CLIP type: {clip_type}") - clip = CLIPLoader().load_clip( - clip_name=clip_name, - type=clip_type, - device=dev - )[0] - print(f"[Rayko] CLIP loaded successfully") + clip = CLIPLoader().load_clip(clip_name=clip_name, type=clip_type, device=dev)[0] if vae_name == "pixel_space": - print(f"[Rayko] Using pixel_space VAE (fake VAE for direct pixel manipulation)") - sd = {} - sd["pixel_space_vae"] = torch.tensor(1.0) + sd = {"pixel_space_vae": torch.tensor(1.0)} vae = comfy.sd.VAE(sd=sd, metadata=None) vae.throw_exception_if_invalid() else: vae = VAELoader().load_vae(vae_name=vae_name)[0] - print(f"[Rayko] VAE: {vae_name}") try: loras = json.loads(lora_data) if lora_data else [] - if not isinstance(loras, list): - loras = [] - print(f"[Rayko] LoRA count: {len(loras)}") - except Exception as e: + if not isinstance(loras, list): loras = [] + except Exception: loras = [] - print(f"[Rayko] Parse error: {e}") lora_loader = LoraLoader() - - for i, lora in enumerate(loras): - if not isinstance(lora, dict): - continue - + for lora in loras: + if not isinstance(lora, dict): continue name = lora.get("name", "") strength_model = float(lora.get("strength_model", 1.0)) strength_clip = float(lora.get("strength_clip", strength_model)) enabled = lora.get("enabled", True) - print(f"[Rayko] LoRA[{i}]: {name} | strength={strength_model} | enabled={enabled}") - if name and name != "None" and enabled: lora_relative_path = name.replace("\\", "/") lora_full_path = folder_paths.get_full_path("loras", lora_relative_path) - if lora_full_path: try: - print(f"[Rayko] Applying LoRA: {lora_relative_path} (strength={strength_model})") - model, clip = lora_loader.load_lora( - model, clip, - lora_relative_path, - strength_model, - strength_clip - ) - print(f"[Rayko] ✓ LoRA applied successfully") + model, clip = lora_loader.load_lora(model, clip, lora_relative_path, strength_model, strength_clip) except Exception as e: - print(f"[Rayko] ✗ Error: {e}") - else: - print(f"[Rayko] ✗ LoRA file not found: {lora_relative_path}") + print(f"[Rayko] ✗ LoRA Error: {e}") - print(f"[Rayko] === load_models completed successfully ===\n") return (model, clip, vae) NODE_CLASS_MAPPINGS = {"RaykoModelsLoader": RaykoModelsLoader} @@ -166,32 +117,18 @@ NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoader": "🦊 RS Models Loader"} @PromptServer.instance.routes.get("/rayko/get_loras") async def get_loras(request): - loras = folder_paths.get_filename_list("loras") - return aiohttp.web.json_response(sorted(loras, key=lambda x: x.lower())) + return aiohttp.web.json_response(sorted(folder_paths.get_filename_list("loras"), key=lambda x: x.lower())) +# --- MODEL PRESETS --- @PromptServer.instance.routes.post("/rayko_models/save_preset") async def rayko_models_save_preset(request): try: data = await request.json() - name = data.get("name", "").strip() - if not name: - return aiohttp.web.Response(status=400, text="Name required") - name = "".join(c for c in name if c.isalnum() or c in " _-").strip() - if not name: - return aiohttp.web.Response(status=400, text="Invalid name") + name = "".join(c for c in data.get("name", "").strip() if c.isalnum() or c in " _-").strip() + if not name: return aiohttp.web.Response(status=400, text="Invalid name") filepath = os.path.join(PRESETS_DIR, f"{name}.json") - preset_data = { - "unet_name": data.get("unet_name", ""), - "weight_dtype": data.get("weight_dtype", "default"), - "use_clip2": data.get("use_clip2", False), - "clip_name": data.get("clip_name", ""), - "clip_name2": data.get("clip_name2", ""), - "clip_type": data.get("clip_type", "stable_diffusion"), - "clip_device": data.get("clip_device", "default"), - "vae_name": data.get("vae_name", "") - } with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, indent=2) + json.dump({k: data.get(k, "") for k in ["unet_name", "weight_dtype", "use_clip2", "clip_name", "clip_name2", "clip_type", "clip_device", "vae_name"]}, f, indent=2) return aiohttp.web.Response(status=200, text="OK") except Exception as e: return aiohttp.web.Response(status=500, text=str(e)) @@ -199,9 +136,7 @@ async def rayko_models_save_preset(request): @PromptServer.instance.routes.post("/rayko_models/list_presets") async def rayko_models_list_presets(request): try: - presets = [] - if os.path.exists(PRESETS_DIR): - presets = [f[:-5] for f in os.listdir(PRESETS_DIR) if f.endswith('.json')] + presets = [f[:-5] for f in os.listdir(PRESETS_DIR) if f.endswith('.json')] if os.path.exists(PRESETS_DIR) else [] return aiohttp.web.json_response(sorted(presets, key=lambda x: x.lower())) except Exception as e: return aiohttp.web.Response(status=500, text=str(e)) @@ -209,13 +144,11 @@ async def rayko_models_list_presets(request): @PromptServer.instance.routes.post("/rayko_models/load_preset") async def rayko_models_load_preset(request): try: - data = await request.json() - name = data.get("name") + name = (await request.json()).get("name") filepath = os.path.join(PRESETS_DIR, f"{name}.json") if os.path.exists(filepath): with open(filepath, 'r', encoding='utf-8') as f: - result = json.load(f) - return aiohttp.web.json_response(result) + return aiohttp.web.json_response(json.load(f)) return aiohttp.web.Response(status=404, text="Preset not found") except Exception as e: return aiohttp.web.Response(status=500, text=str(e)) @@ -223,14 +156,57 @@ async def rayko_models_load_preset(request): @PromptServer.instance.routes.post("/rayko_models/delete_preset") async def rayko_models_delete_preset(request): try: - data = await request.json() - name = data.get("name") - if not name: - return aiohttp.web.Response(status=400, text="Name required") + name = (await request.json()).get("name") filepath = os.path.join(PRESETS_DIR, f"{name}.json") if os.path.exists(filepath): os.remove(filepath) return aiohttp.web.Response(status=200, text="OK") return aiohttp.web.Response(status=404, text="Preset not found") + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +# --- LORA PRESETS --- +@PromptServer.instance.routes.post("/rayko_loras/save_preset") +async def rayko_loras_save_preset(request): + try: + data = await request.json() + name = "".join(c for c in data.get("name", "").strip() if c.isalnum() or c in " _-").strip() + if not name: return aiohttp.web.Response(status=400, text="Invalid name") + filepath = os.path.join(LORA_PRESETS_DIR, f"{name}.json") + with open(filepath, 'w', encoding='utf-8') as f: + json.dump({"lora_rows": data.get("lora_rows", [])}, f, indent=2) + return aiohttp.web.Response(status=200, text="OK") + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +@PromptServer.instance.routes.post("/rayko_loras/list_presets") +async def rayko_loras_list_presets(request): + try: + presets = [f[:-5] for f in os.listdir(LORA_PRESETS_DIR) if f.endswith('.json')] if os.path.exists(LORA_PRESETS_DIR) else [] + return aiohttp.web.json_response(sorted(presets, key=lambda x: x.lower())) + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +@PromptServer.instance.routes.post("/rayko_loras/load_preset") +async def rayko_loras_load_preset(request): + try: + name = (await request.json()).get("name") + filepath = os.path.join(LORA_PRESETS_DIR, f"{name}.json") + if os.path.exists(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + return aiohttp.web.json_response(json.load(f)) + return aiohttp.web.Response(status=404, text="Preset not found") + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +@PromptServer.instance.routes.post("/rayko_loras/delete_preset") +async def rayko_loras_delete_preset(request): + try: + name = (await request.json()).get("name") + filepath = os.path.join(LORA_PRESETS_DIR, f"{name}.json") + if os.path.exists(filepath): + os.remove(filepath) + return aiohttp.web.Response(status=200, text="OK") + return aiohttp.web.Response(status=404, text="Preset not found") except Exception as e: return aiohttp.web.Response(status=500, text=str(e)) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 2cbb5ab..f3bbbed 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.30.0" +__version__ = "0.30.1" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 58e4cc6..a5ad005 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.30.0", + "version": "0.30.1", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index b35816d..38abae6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.30.0" +version = "0.30.1" license = { file = "LICENSE" } dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"] diff --git a/web/rayko_lora_widget.js b/web/rayko_lora_widget.js index 62b060d..685cfc4 100644 --- a/web/rayko_lora_widget.js +++ b/web/rayko_lora_widget.js @@ -40,25 +40,15 @@ app.registerExtension({ if (useClip2Widget && clipName2Widget) { clipName2Widget.disabled = !useClip2Widget.value; - if (clipName2Widget.element) { - if (clipName2Widget.disabled) { - clipName2Widget.element.classList.add("comfy-disabled"); - } else { - clipName2Widget.element.classList.remove("comfy-disabled"); - } + clipName2Widget.element.classList.toggle("comfy-disabled", clipName2Widget.disabled); } - const originalCallback = useClip2Widget.callback; useClip2Widget.callback = function(value) { if (originalCallback) originalCallback(value); clipName2Widget.disabled = !value; if (clipName2Widget.element) { - if (clipName2Widget.disabled) { - clipName2Widget.element.classList.add("comfy-disabled"); - } else { - clipName2Widget.element.classList.remove("comfy-disabled"); - } + clipName2Widget.element.classList.toggle("comfy-disabled", clipName2Widget.disabled); } nodeRef.graph?.setDirtyCanvas(true, true); }; @@ -66,18 +56,16 @@ app.registerExtension({ this.setSize([this.targetWidth, this.size[1]]); - const collectPresetData = () => { - return { - unet_name: this.widgets.find(w => w.name === "unet_name")?.value || "", - weight_dtype: this.widgets.find(w => w.name === "weight_dtype")?.value || "default", - use_clip2: this.widgets.find(w => w.name === "use_clip2")?.value || false, - clip_name: this.widgets.find(w => w.name === "clip_name")?.value || "", - clip_name2: this.widgets.find(w => w.name === "clip_name2")?.value || "", - clip_type: this.widgets.find(w => w.name === "clip_type")?.value || "stable_diffusion", - clip_device: this.widgets.find(w => w.name === "clip_device")?.value || "default", - vae_name: this.widgets.find(w => w.name === "vae_name")?.value || "" - }; - }; + const collectPresetData = () => ({ + unet_name: this.widgets.find(w => w.name === "unet_name")?.value || "", + weight_dtype: this.widgets.find(w => w.name === "weight_dtype")?.value || "default", + use_clip2: this.widgets.find(w => w.name === "use_clip2")?.value || false, + clip_name: this.widgets.find(w => w.name === "clip_name")?.value || "", + clip_name2: this.widgets.find(w => w.name === "clip_name2")?.value || "", + clip_type: this.widgets.find(w => w.name === "clip_type")?.value || "stable_diffusion", + clip_device: this.widgets.find(w => w.name === "clip_device")?.value || "default", + vae_name: this.widgets.find(w => w.name === "vae_name")?.value || "" + }); const applyPresetData = (data) => { const setWidgetValue = (name, value) => { @@ -87,21 +75,30 @@ app.registerExtension({ if (widget.callback) widget.callback(value); } }; - - setWidgetValue("unet_name", data.unet_name); - setWidgetValue("weight_dtype", data.weight_dtype); - setWidgetValue("use_clip2", data.use_clip2); - setWidgetValue("clip_name", data.clip_name); - setWidgetValue("clip_name2", data.clip_name2); - setWidgetValue("clip_type", data.clip_type); - setWidgetValue("clip_device", data.clip_device); - setWidgetValue("vae_name", data.vae_name); - + ["unet_name", "weight_dtype", "use_clip2", "clip_name", "clip_name2", "clip_type", "clip_device", "vae_name"].forEach(k => setWidgetValue(k, data[k])); this.loraRows = []; this.syncData(); - this.updateUI(); - if (this.graph) this.graph.setDirtyCanvas(true, true); + }; + + const collectLoraPresetData = () => ({ + lora_rows: this.loraRows.map(row => ({ + name: row.name, + strength_model: row.strength_model, + strength_clip: row.strength_clip, + enabled: row.enabled + })) + }); + + const applyLoraPresetData = (data) => { + this.loraRows = (data.lora_rows || []).map(row => ({ + name: row.name || "", + strength_model: parseFloat(row.strength_model) || 1.0, + strength_clip: parseFloat(row.strength_clip) || 1.0, + enabled: row.enabled !== undefined ? row.enabled : true + })); + this.syncData(); + this.updateUI(); }; const presetListOverlay = document.createElement("div"); @@ -154,75 +151,101 @@ app.registerExtension({ deleteConfirmOverlay.append(deleteText, deleteBtns); document.body.appendChild(deleteConfirmOverlay); + let pendingSaveType = "model"; + let pendingSelectType = "model"; + let pendingDeleteType = "model"; let pendingDeleteName = null; + const presetsWrapper = document.createElement("div"); + presetsWrapper.style.cssText = "display:flex;flex-direction:column;gap:4px;width:100%;margin:0;padding:0;box-sizing:border-box;"; + + // --- MODEL PRESETS ROW --- const presetsRoot = document.createElement("div"); - presetsRoot.style.cssText = "display:flex;gap:4px;width:100%;margin:0;padding:0;box-sizing:border-box;height:40px;align-items:center;"; + presetsRoot.style.cssText = "display:flex;gap:4px;width:100%;align-items:center;height:30px;"; const savePresetBtn = document.createElement("button"); - savePresetBtn.textContent = " Save preset"; + savePresetBtn.textContent = " 💾 Save models preset"; savePresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;height:26px;margin:0;"; const selectPresetBtn = document.createElement("button"); - selectPresetBtn.textContent = "📂 Select preset"; + selectPresetBtn.textContent = " 📂 Select models preset"; selectPresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;height:26px;margin:0;"; - presetsRoot.append(savePresetBtn, selectPresetBtn); - savePresetBtn.addEventListener("click", (e) => { - e.stopPropagation(); + // --- LORA PRESETS ROW (Pale Green) --- + const loraPresetsRoot = document.createElement("div"); + loraPresetsRoot.style.cssText = "display:flex;gap:4px;width:100%;align-items:center;height:30px;"; + + const saveLoraPresetBtn = document.createElement("button"); + saveLoraPresetBtn.textContent = " 💾 Save LoRA preset"; + saveLoraPresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #50cc90;border-radius:5px;background:#1a3a2a;color:#aaffcc;cursor:pointer;height:26px;margin:0;"; + + const selectLoraPresetBtn = document.createElement("button"); + selectLoraPresetBtn.textContent = " 📂 Select LoRA preset"; + selectLoraPresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #50cc90;border-radius:5px;background:#1a3a2a;color:#aaffcc;cursor:pointer;height:26px;margin:0;"; + loraPresetsRoot.append(saveLoraPresetBtn, selectLoraPresetBtn); + + presetsWrapper.append(presetsRoot, loraPresetsRoot); + + const presetsWidget = this.addDOMWidget("presets_ui", "custom", presetsWrapper); + presetsWidget.computeSize = function() { return [this.width || 130, 70]; }; + + const openSaveDialog = (type, btnElement) => { presetListOverlay.style.display = "none"; deleteConfirmOverlay.style.display = "none"; - - const saveBtnRect = savePresetBtn.getBoundingClientRect(); - presetNameInput.style.left = saveBtnRect.left + "px"; - presetNameInput.style.top = (saveBtnRect.bottom + 5) + "px"; + pendingSaveType = type; + const rect = btnElement.getBoundingClientRect(); + presetNameInput.style.left = rect.left + "px"; + presetNameInput.style.top = (rect.bottom + 5) + "px"; presetNameInput.style.display = "block"; inputField.value = ""; setTimeout(() => inputField.focus(), 50); - }); + }; + + savePresetBtn.addEventListener("click", (e) => { e.stopPropagation(); openSaveDialog("model", savePresetBtn); }); + saveLoraPresetBtn.addEventListener("click", (e) => { e.stopPropagation(); openSaveDialog("lora", saveLoraPresetBtn); }); const performSave = () => { const name = inputField.value.trim(); if (!name) return; presetNameInput.style.display = "none"; - const presetData = collectPresetData(); - fetch("/rayko_models/save_preset", { + + const endpoint = pendingSaveType === "model" ? "/rayko_models/save_preset" : "/rayko_loras/save_preset"; + const payload = pendingSaveType === "model" ? { name, ...collectPresetData() } : { name, ...collectLoraPresetData() }; + + fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, ...presetData }) + body: JSON.stringify(payload) + }) + .then(res => { + if (!res.ok) { + return res.text().then(text => { throw new Error(text || res.statusText); }); + } + }) + .catch(err => { + console.error(`[Rayko] Failed to save preset:`, err); + alert(`Failed to save preset:\n${err.message}`); }); }; inputOk.addEventListener("click", performSave); - inputCancel.addEventListener("click", () => { - presetNameInput.style.display = "none"; - }); + inputCancel.addEventListener("click", () => { presetNameInput.style.display = "none"; }); inputField.addEventListener("keydown", (e) => { if (e.key === "Enter") performSave(); if (e.key === "Escape") presetNameInput.style.display = "none"; }); - selectPresetBtn.addEventListener("click", async (e) => { - e.stopPropagation(); - presetNameInput.style.display = "none"; - deleteConfirmOverlay.style.display = "none"; - if (presetListOverlay.style.display === "flex") { - presetListOverlay.style.display = "none"; - return; - } - - const selectBtnRect = selectPresetBtn.getBoundingClientRect(); - presetListOverlay.style.left = selectBtnRect.left + "px"; - presetListOverlay.style.top = (selectBtnRect.bottom + 5) + "px"; - + const showPresetList = async (btnElement) => { + const rect = btnElement.getBoundingClientRect(); + presetListOverlay.style.left = rect.left + "px"; + presetListOverlay.style.top = (rect.bottom + 5) + "px"; presetListOverlay.innerHTML = "
Loading...
"; presetListOverlay.style.display = "flex"; try { - const res = await fetch("/rayko_models/list_presets", { - method: "POST", - headers: { "Content-Type": "application/json" } - }); + const endpoint = pendingSelectType === "model" ? "/rayko_models/list_presets" : "/rayko_loras/list_presets"; + const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" } }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); const list = await res.json(); presetListOverlay.innerHTML = ""; if (!list.length) { @@ -241,91 +264,92 @@ app.registerExtension({ nameSpan.onclick = async (e) => { e.stopPropagation(); presetListOverlay.style.display = "none"; - const res2 = await fetch("/rayko_models/load_preset", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name }) - }); - if (res2.ok) { + const loadEndpoint = pendingSelectType === "model" ? "/rayko_models/load_preset" : "/rayko_loras/load_preset"; + try { + const res2 = await fetch(loadEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }) }); + if (!res2.ok) throw new Error(`HTTP ${res2.status}`); const data = await res2.json(); - applyPresetData(data); + if (pendingSelectType === "model") applyPresetData(data); + else applyLoraPresetData(data); + } catch (err) { + console.error(`[Rayko] Failed to load preset:`, err); + alert(`Failed to load preset: ${err.message}`); } }; - const deleteBtn = document.createElement("span"); - deleteBtn.textContent = "❌"; - deleteBtn.style.cssText = "cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;"; - deleteBtn.onmouseenter = () => { - deleteBtn.style.opacity = "1"; - deleteBtn.style.transform = "scale(1.2)"; - }; - deleteBtn.onmouseleave = () => { - deleteBtn.style.opacity = "0.7"; - deleteBtn.style.transform = "scale(1)"; - }; - deleteBtn.onclick = (e) => { + const delBtn = document.createElement("span"); + delBtn.textContent = "❌"; + delBtn.style.cssText = "cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;"; + delBtn.onmouseenter = () => { delBtn.style.opacity = "1"; delBtn.style.transform = "scale(1.2)"; }; + delBtn.onmouseleave = () => { delBtn.style.opacity = "0.7"; delBtn.style.transform = "scale(1)"; }; + delBtn.onclick = (e) => { e.stopPropagation(); pendingDeleteName = name; + pendingDeleteType = pendingSelectType; deleteText.textContent = `Delete "${name}"?`; - - const selectBtnRect = selectPresetBtn.getBoundingClientRect(); - deleteConfirmOverlay.style.left = selectBtnRect.left + "px"; - deleteConfirmOverlay.style.top = (selectBtnRect.bottom + 5) + "px"; + const r = btnElement.getBoundingClientRect(); + deleteConfirmOverlay.style.left = r.left + "px"; + deleteConfirmOverlay.style.top = (r.bottom + 5) + "px"; deleteConfirmOverlay.style.display = "block"; }; - row.appendChild(nameSpan); - row.appendChild(deleteBtn); + row.appendChild(delBtn); presetListOverlay.appendChild(row); }); } catch (e) { + console.error(`[Rayko] Failed to list presets:`, e); presetListOverlay.textContent = "Error loading"; } + }; + + selectPresetBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + presetNameInput.style.display = "none"; + deleteConfirmOverlay.style.display = "none"; + if (presetListOverlay.style.display === "flex") { presetListOverlay.style.display = "none"; return; } + pendingSelectType = "model"; + showPresetList(selectPresetBtn); + }); + + selectLoraPresetBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + presetNameInput.style.display = "none"; + deleteConfirmOverlay.style.display = "none"; + if (presetListOverlay.style.display === "flex") { presetListOverlay.style.display = "none"; return; } + pendingSelectType = "lora"; + showPresetList(selectLoraPresetBtn); }); deleteOk.addEventListener("click", async () => { if (pendingDeleteName) { - await fetch("/rayko_models/delete_preset", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: pendingDeleteName }) - }); - deleteConfirmOverlay.style.display = "none"; - presetListOverlay.style.display = "none"; - pendingDeleteName = null; + const endpoint = pendingDeleteType === "model" ? "/rayko_models/delete_preset" : "/rayko_loras/delete_preset"; + try { + const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: pendingDeleteName }) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + deleteConfirmOverlay.style.display = "none"; + presetListOverlay.style.display = "none"; + pendingDeleteName = null; + } catch (err) { + console.error(`[Rayko] Failed to delete preset:`, err); + alert(`Failed to delete preset: ${err.message}`); + } } }); - deleteCancel.addEventListener("click", () => { - deleteConfirmOverlay.style.display = "none"; - pendingDeleteName = null; - }); + deleteCancel.addEventListener("click", () => { deleteConfirmOverlay.style.display = "none"; pendingDeleteName = null; }); document.addEventListener("click", (e) => { - if (!presetListOverlay?.contains(e.target)) { - presetListOverlay.style.display = "none"; - } - if (!presetNameInput?.contains(e.target)) { - presetNameInput.style.display = "none"; - } - if (!deleteConfirmOverlay?.contains(e.target)) { - deleteConfirmOverlay.style.display = "none"; - } + if (!presetListOverlay?.contains(e.target)) presetListOverlay.style.display = "none"; + if (!presetNameInput?.contains(e.target)) presetNameInput.style.display = "none"; + if (!deleteConfirmOverlay?.contains(e.target)) deleteConfirmOverlay.style.display = "none"; }); - const presetsWidget = this.addDOMWidget("presets_ui", "custom", presetsRoot); - presetsWidget.computeSize = function() { - return [this.width || 130, 50]; - }; - this.addWidget("button", "✔️ Update LoRA list", "", async () => { await this.loadLoraList(); if (this.graph) this.graph.setDirtyCanvas(true, true); }); - this.addWidget("button", "➕ Add LoRA", "", () => { - this.showLoraTreeSelector(); - }); + this.addWidget("button", "➕ Add LoRA", "", () => { this.showLoraTreeSelector(); }); const self = this; setTimeout(() => { @@ -373,24 +397,16 @@ app.registerExtension({ if (useClip2Widget && clipName2Widget) { clipName2Widget.disabled = !useClip2Widget.value; if (clipName2Widget.element) { - if (clipName2Widget.disabled) { - clipName2Widget.element.classList.add("comfy-disabled"); - } else { - clipName2Widget.element.classList.remove("comfy-disabled"); - } + clipName2Widget.element.classList.toggle("comfy-disabled", clipName2Widget.disabled); } } - return originalOnConfigure ? originalOnConfigure.apply(this, arguments) : undefined; }; nodeType.prototype.saveToStorage = function() { if (!this.storageKey) return; try { - localStorage.setItem(this.storageKey, JSON.stringify({ - loraRows: this.loraRows, - timestamp: Date.now() - })); + localStorage.setItem(this.storageKey, JSON.stringify({ loraRows: this.loraRows, timestamp: Date.now() })); } catch (e) {} }; @@ -400,9 +416,7 @@ app.registerExtension({ const stored = localStorage.getItem(this.storageKey); if (stored) { const data = JSON.parse(stored); - if (Date.now() - data.timestamp < 86400000 && Array.isArray(data.loraRows)) { - return data.loraRows; - } + if (Date.now() - data.timestamp < 86400000 && Array.isArray(data.loraRows)) return data.loraRows; } } catch (e) {} return null; @@ -414,19 +428,13 @@ app.registerExtension({ if (this.properties && this.properties["lora_rows"]) { try { savedData = JSON.parse(this.properties["lora_rows"]); - if (Array.isArray(savedData) && savedData.length > 0) { - this.loraRows = savedData; - return; - } + if (Array.isArray(savedData) && savedData.length > 0) { this.loraRows = savedData; return; } } catch (e) {} } if (!savedData && this.hiddenWidget && this.hiddenWidget.value) { try { const widgetVal = JSON.parse(this.hiddenWidget.value); - if (Array.isArray(widgetVal) && widgetVal.length > 0) { - this.loraRows = widgetVal; - return; - } + if (Array.isArray(widgetVal) && widgetVal.length > 0) { this.loraRows = widgetVal; return; } } catch (e) {} } if (!savedData) { @@ -467,7 +475,6 @@ app.registerExtension({ nodeType.prototype.onDrawForeground = function(ctx, visibleRect) { if (this.loraRows.length === 0) return; this.clickZones = []; - const addButton = this.widgets.find(w => w.name === "➕ Add LoRA"); const startY = addButton ? (addButton.y + addButton.height + 15) : 40; const padding = 10;