Upgrading the RS Models Loader node

This commit is contained in:
Raykosan
2026-07-21 18:38:08 +03:00
parent cacea98284
commit 2ad46f68d0
5 changed files with 225 additions and 242 deletions
+69 -93
View File
@@ -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))
+1 -1
View File
@@ -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__}")
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"]
+153 -146
View File
@@ -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 = "<div style='padding:8px;color:#999;text-align:center;'>Loading...</div>";
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;