lora preview rework
+toggle Low +checkpoint loader with ui
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ import re
|
||||
|
||||
# Import everything from the nodes directory
|
||||
from .nodes.nodes import *
|
||||
from .nodes.power_loras import *
|
||||
from .nodes.power_loaders import *
|
||||
|
||||
from .nodes.power_spline_editor import *
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import os
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
import folder_paths
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
import os
|
||||
@@ -70,3 +72,219 @@ async def save_ref_image(request):
|
||||
traceback.print_exc()
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
def get_lora_full_path(lora_name):
|
||||
"""Get the full path to a LoRA file"""
|
||||
try:
|
||||
# Get the LoRA folder paths from ComfyUI
|
||||
lora_folders = folder_paths.get_folder_paths("loras")
|
||||
|
||||
# Search for the LoRA file in all LoRA folders
|
||||
for folder in lora_folders:
|
||||
lora_path = os.path.join(folder, lora_name)
|
||||
if os.path.exists(lora_path):
|
||||
return os.path.abspath(lora_path)
|
||||
|
||||
# Also check for common extensions if not provided
|
||||
if not lora_name.lower().endswith(('.safetensors', '.ckpt', '.pt')):
|
||||
for ext in ['.safetensors', '.ckpt', '.pt']:
|
||||
lora_path_with_ext = os.path.join(folder, lora_name + ext)
|
||||
if os.path.exists(lora_path_with_ext):
|
||||
return os.path.abspath(lora_path_with_ext)
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error finding LoRA path: {e}")
|
||||
return None
|
||||
|
||||
@PromptServer.instance.routes.post("/wanvideo_wrapper/open_explorer")
|
||||
async def open_explorer(request):
|
||||
"""API endpoint to open Windows Explorer at the LoRA file location"""
|
||||
try:
|
||||
if not sys.platform.startswith('win'):
|
||||
return web.json_response({"error": "This feature is only available on Windows"}, status=400)
|
||||
|
||||
post = await request.post()
|
||||
data = await request.json() if request.content_type == 'application/json' else {}
|
||||
|
||||
lora_name = data.get("lora_name") or post.get("lora_name")
|
||||
|
||||
if not lora_name:
|
||||
return web.json_response({"error": "No LoRA name provided"}, status=400)
|
||||
|
||||
# Get the full path to the LoRA file
|
||||
lora_path = get_lora_full_path(lora_name)
|
||||
|
||||
if not lora_path:
|
||||
return web.json_response({"error": f"LoRA file not found: {lora_name}"}, status=404)
|
||||
|
||||
# Open Windows Explorer and select the file
|
||||
try:
|
||||
# Use start command to open Explorer with the file selected
|
||||
subprocess.run(['explorer', '/select,', lora_path], check=True, shell=False)
|
||||
return web.json_response({"success": True, "path": lora_path})
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Fallback: open the folder if selecting the file fails
|
||||
try:
|
||||
folder_path = os.path.dirname(lora_path)
|
||||
subprocess.run(['explorer', folder_path], check=True, shell=False)
|
||||
return web.json_response({"success": True, "path": folder_path, "message": "Opened folder instead of selecting file"})
|
||||
except subprocess.CalledProcessError as e2:
|
||||
return web.json_response({"error": f"Failed to open Explorer: {str(e2)}"}, status=500)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error opening Explorer: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.post("/wanvideo_wrapper/get_lora_path")
|
||||
async def get_lora_path(request):
|
||||
"""API endpoint to get the full path of a LoRA file"""
|
||||
try:
|
||||
post = await request.post()
|
||||
data = await request.json() if request.content_type == 'application/json' else {}
|
||||
|
||||
lora_name = data.get("lora_name") or post.get("lora_name")
|
||||
|
||||
if not lora_name:
|
||||
return web.json_response({"error": "No LoRA name provided"}, status=400)
|
||||
|
||||
# Get the full path to the LoRA file
|
||||
lora_path = get_lora_full_path(lora_name)
|
||||
|
||||
if not lora_path:
|
||||
return web.json_response({"error": f"LoRA file not found: {lora_name}"}, status=404)
|
||||
|
||||
return web.json_response({"success": True, "path": lora_path})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting LoRA path: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.post("/wanvideo_wrapper/rename_lora")
|
||||
async def rename_lora(request):
|
||||
"""API endpoint to rename a LoRA file and its associated files"""
|
||||
try:
|
||||
post = await request.post()
|
||||
data = await request.json() if request.content_type == 'application/json' else {}
|
||||
|
||||
old_name = data.get("old_name") or post.get("old_name")
|
||||
new_name = data.get("new_name") or post.get("new_name")
|
||||
variant = data.get("variant") or post.get("variant", "high")
|
||||
|
||||
if not old_name:
|
||||
return web.json_response({"error": "No old LoRA name provided"}, status=400)
|
||||
|
||||
if not new_name:
|
||||
return web.json_response({"error": "No new LoRA name provided"}, status=400)
|
||||
|
||||
# Get the full path to the LoRA file
|
||||
old_lora_path = get_lora_full_path(old_name)
|
||||
|
||||
if not old_lora_path:
|
||||
return web.json_response({"error": f"LoRA file not found: {old_name}"}, status=404)
|
||||
|
||||
# Get the directory and file extension
|
||||
lora_dir = os.path.dirname(old_lora_path)
|
||||
old_base_name = os.path.splitext(os.path.basename(old_lora_path))[0]
|
||||
lora_ext = os.path.splitext(old_lora_path)[1]
|
||||
|
||||
# Create the new file paths
|
||||
new_lora_path = os.path.join(lora_dir, f"{new_name}{lora_ext}")
|
||||
|
||||
# Check if new file already exists
|
||||
if os.path.exists(new_lora_path):
|
||||
return web.json_response({"error": f"A file with the name '{new_name}{lora_ext}' already exists"}, status=409)
|
||||
|
||||
# Rename the main LoRA file
|
||||
try:
|
||||
os.rename(old_lora_path, new_lora_path)
|
||||
print(f"Renamed LoRA: {old_lora_path} -> {new_lora_path}")
|
||||
except Exception as e:
|
||||
return web.json_response({"error": f"Failed to rename LoRA file: {str(e)}"}, status=500)
|
||||
|
||||
# Rename associated files (JSON and preview images)
|
||||
renamed_files = [new_lora_path]
|
||||
|
||||
# Determine if this is a model or LoRA based on file path
|
||||
is_model = any(old_lora_path.startswith(folder) for folder in folder_paths.get_folder_paths("checkpoints"))
|
||||
|
||||
if is_model:
|
||||
# Handle model files - check checkpoints directory
|
||||
model_folders = folder_paths.get_folder_paths("checkpoints")
|
||||
for folder in model_folders:
|
||||
# Look for _power_preview subfolder
|
||||
preview_folder = os.path.join(folder, "_power_preview")
|
||||
if os.path.exists(preview_folder):
|
||||
renamed_files.extend(_rename_preview_files(old_base_name, new_name, preview_folder))
|
||||
else:
|
||||
# Handle LoRA files - check loras directory
|
||||
lora_folders = folder_paths.get_folder_paths("loras")
|
||||
for folder in lora_folders:
|
||||
# Look for _power_preview subfolder
|
||||
preview_folder = os.path.join(folder, "_power_preview")
|
||||
if os.path.exists(preview_folder):
|
||||
renamed_files.extend(_rename_preview_files(old_base_name, new_name, preview_folder))
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": f"Successfully renamed LoRA and {len(renamed_files)-1} associated files",
|
||||
"new_high_path": new_lora_path,
|
||||
"new_low_path": new_lora_path, # For high variant, low path is the same
|
||||
"renamed_files": renamed_files
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error renaming LoRA: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
def _rename_preview_files(old_base_name, new_name, preview_folder):
|
||||
"""Helper function to rename preview files (JSON and images) in a preview folder."""
|
||||
renamed_files = []
|
||||
|
||||
try:
|
||||
# Rename JSON file
|
||||
old_json_path = os.path.join(preview_folder, f"{old_base_name}.json")
|
||||
new_json_path = os.path.join(preview_folder, f"{new_name}.json")
|
||||
if os.path.exists(old_json_path):
|
||||
try:
|
||||
os.rename(old_json_path, new_json_path)
|
||||
renamed_files.append(new_json_path)
|
||||
print(f"Renamed JSON: {old_json_path} -> {new_json_path}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to rename JSON file: {e}")
|
||||
|
||||
# Rename preview images (with various suffixes)
|
||||
for suffix in ["_01", "_02", "_03", "_thumb"]:
|
||||
for ext in [".jpg", ".jpeg", ".png", ".webp"]:
|
||||
old_img_path = os.path.join(preview_folder, f"{old_base_name}{suffix}{ext}")
|
||||
new_img_path = os.path.join(preview_folder, f"{new_name}{suffix}{ext}")
|
||||
if os.path.exists(old_img_path):
|
||||
try:
|
||||
os.rename(old_img_path, new_img_path)
|
||||
renamed_files.append(new_img_path)
|
||||
print(f"Renamed image: {old_img_path} -> {new_img_path}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to rename image file: {e}")
|
||||
|
||||
# Also check for image without suffix (backward compatibility)
|
||||
for ext in [".jpg", ".jpeg", ".png", ".webp"]:
|
||||
old_img_path = os.path.join(preview_folder, f"{old_base_name}{ext}")
|
||||
new_img_path = os.path.join(preview_folder, f"{new_name}{ext}")
|
||||
if os.path.exists(old_img_path):
|
||||
try:
|
||||
os.rename(old_img_path, new_img_path)
|
||||
renamed_files.append(new_img_path)
|
||||
print(f"Renamed image: {old_img_path} -> {new_img_path}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to rename image file: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in _rename_preview_files: {e}")
|
||||
|
||||
return renamed_files
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from typing import Union
|
||||
import folder_paths
|
||||
import comfy.sd
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
@@ -236,7 +237,7 @@ class WanVideoPowerLoraLoader:
|
||||
# Add to high loras summary
|
||||
high_loras_summary.append((strength_model, lora_filename))
|
||||
|
||||
if is_low and low_variant_name:
|
||||
if is_low and low_variant_name and value.get('low_active', True):
|
||||
# Use low_strength for the low variant LoRA, fallback to strength_model if not available
|
||||
low_strength = value.get('low_strength', strength_model)
|
||||
# Ensure low_strength is not None
|
||||
@@ -443,7 +444,7 @@ class PowerLoraLoaderV2:
|
||||
# Add to high loras summary
|
||||
high_loras_summary.append((strength_model, lora_filename))
|
||||
|
||||
if is_low and low_variant_name:
|
||||
if is_low and low_variant_name and value.get('low_active', True):
|
||||
# Use low_strength for the low variant LoRA, fallback to strength_model if not available
|
||||
low_strength = value.get('low_strength', strength_model)
|
||||
# Ensure low_strength is not None
|
||||
@@ -503,12 +504,65 @@ class PowerLoraLoaderV2:
|
||||
# Clip connected, return all three outputs
|
||||
return (model, model_low, clip)
|
||||
|
||||
class CheckpointLoader_v2:
|
||||
"""A power checkpoint loader v2 that provides PowerLoraLoaderV2-style UI for model checkpoints."""
|
||||
|
||||
def __init__(self):
|
||||
self.properties = {}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": FlexibleOptionalInputType(type=any_type, data={}),
|
||||
"hidden": {
|
||||
"ui_state": ("STRING", {"default": "{}"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
|
||||
RETURN_NAMES = ("MODEL", "CLIP", "VAE")
|
||||
FUNCTION = "load_checkpoint"
|
||||
CATEGORY = "WanVideoWrapper_QQ/loaders"
|
||||
DESCRIPTION = "Power Checkpoint Loader v2 - Load model checkpoints with advanced UI"
|
||||
|
||||
def load_checkpoint(self, **kwargs):
|
||||
"""Load the selected checkpoint model."""
|
||||
# Find the checkpoint widget data
|
||||
checkpoint_name = None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key.startswith('model_') and isinstance(value, dict) and 'on' in value and 'model' in value:
|
||||
# Check if model is enabled and has valid data
|
||||
if not value['on']:
|
||||
continue
|
||||
|
||||
model_name = value['model']
|
||||
if not model_name or model_name.lower() == "none":
|
||||
continue
|
||||
|
||||
checkpoint_name = model_name
|
||||
break
|
||||
|
||||
if checkpoint_name is None:
|
||||
raise ValueError("No checkpoint model selected or enabled")
|
||||
|
||||
# Use ComfyUI's CheckpointLoaderSimple to load the checkpoint
|
||||
try:
|
||||
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", checkpoint_name)
|
||||
out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
|
||||
return out[:3]
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to load checkpoint '{checkpoint_name}': {str(e)}")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"WanVideoPowerLoraLoader": WanVideoPowerLoraLoader,
|
||||
"PowerLoraLoaderV2": PowerLoraLoaderV2,
|
||||
"CheckpointLoader_v2": CheckpointLoader_v2,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"WanVideoPowerLoraLoader": "Wan Video Power Lora Loader",
|
||||
"PowerLoraLoaderV2": "Power Lora Loader V2",
|
||||
"CheckpointLoader_v2": "Power Checkpoint Loader V2",
|
||||
}
|
||||
@@ -180,6 +180,7 @@ async def api_get_lora_preview_image(request):
|
||||
file_param = get_param(request, 'file')
|
||||
subfolder_param = get_param(request, 'subfolder', '')
|
||||
suffix_param = get_param(request, 'suffix', '') # Optional suffix parameter
|
||||
is_model_param = get_param(request, 'is_model', 'false').lower() == 'true'
|
||||
|
||||
if not file_param:
|
||||
api_response['status'] = '400'
|
||||
@@ -192,11 +193,16 @@ async def api_get_lora_preview_image(request):
|
||||
api_response['error'] = 'Invalid file path'
|
||||
return web.json_response(api_response)
|
||||
|
||||
# Get loras directory
|
||||
loras_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
|
||||
# Create _power_preview directory structure (same as save function)
|
||||
power_preview_dir = os.path.join(loras_dir, '_power_preview')
|
||||
# Get appropriate directory based on type
|
||||
if is_model_param:
|
||||
# For models, use checkpoints directory
|
||||
base_dir = folder_paths.get_folder_paths('checkpoints')[0]
|
||||
# Use _power_preview subdirectory for model previews
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
else:
|
||||
# For LoRAs, use loras directory
|
||||
base_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
|
||||
# Construct the full path
|
||||
if subfolder_param:
|
||||
@@ -292,23 +298,32 @@ async def api_get_lora_previews_list(request):
|
||||
api_response = {'status': 200, 'previews': []}
|
||||
|
||||
try:
|
||||
# Get loras directory
|
||||
loras_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
power_preview_dir = os.path.join(loras_dir, '_power_preview')
|
||||
# Get parameters to determine if we're looking for models or LoRAs
|
||||
is_model_param = get_param(request, 'is_model', 'false').lower() == 'true'
|
||||
|
||||
if is_model_param:
|
||||
# Handle models (use checkpoints directory)
|
||||
base_dir = folder_paths.get_folder_paths('checkpoints')[0]
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
files_list = folder_paths.get_filename_list('checkpoints')
|
||||
else:
|
||||
# Handle LoRAs
|
||||
base_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
files_list = folder_paths.get_filename_list('loras')
|
||||
|
||||
if not path_exists(power_preview_dir):
|
||||
return web.json_response(api_response)
|
||||
# Ensure directory exists if it doesn't
|
||||
os.makedirs(power_preview_dir, exist_ok=True)
|
||||
print(f"[Preview] Created directory: {power_preview_dir}")
|
||||
|
||||
# Get all LoRA files
|
||||
lora_files = folder_paths.get_filename_list('loras')
|
||||
|
||||
# Check each LoRA for preview images
|
||||
for lora_file in lora_files:
|
||||
lora_name = lora_file
|
||||
# Check each file for preview images
|
||||
for file_item in files_list:
|
||||
item_name = file_item
|
||||
# Remove extension
|
||||
for ext in ['.safetensors', '.pt', '.ckpt']:
|
||||
if lora_name.endswith(ext):
|
||||
lora_name = lora_name[:-len(ext)]
|
||||
for ext in ['.safetensors', '.pt', '.ckpt', '.bin']:
|
||||
if item_name.endswith(ext):
|
||||
item_name = item_name[:-len(ext)]
|
||||
break
|
||||
|
||||
# Check for preview in various locations
|
||||
@@ -322,15 +337,16 @@ async def api_get_lora_previews_list(request):
|
||||
|
||||
# Check root _power_preview directory
|
||||
for img_ext in ['jpg', 'jpeg', 'png']:
|
||||
preview_path = os.path.join(power_preview_dir, f"{lora_name}{suffix}.{img_ext}")
|
||||
preview_path = os.path.join(power_preview_dir, f"{item_name}{suffix}.{img_ext}")
|
||||
if path_exists(preview_path):
|
||||
preview_found = True
|
||||
preview_paths.append(f"_power_preview/{lora_name}{suffix}.{img_ext}")
|
||||
relative_path = os.path.relpath(preview_path, base_dir)
|
||||
preview_paths.append(relative_path.replace('\\', '/'))
|
||||
found_suffixes.append(suffix)
|
||||
break
|
||||
|
||||
# Check subfolders
|
||||
path_parts = lora_name.replace('/', os.sep).replace('\\', os.sep).split(os.sep)
|
||||
path_parts = item_name.replace('/', os.sep).replace('\\', os.sep).split(os.sep)
|
||||
if len(path_parts) > 1:
|
||||
subfolder = os.sep.join(path_parts[:-1])
|
||||
filename = path_parts[-1]
|
||||
@@ -341,7 +357,8 @@ async def api_get_lora_previews_list(request):
|
||||
preview_path = os.path.join(subfolder_path, f"{filename}{suffix}.{img_ext}")
|
||||
if path_exists(preview_path):
|
||||
preview_found = True
|
||||
preview_paths.append(f"_power_preview/{subfolder}/{filename}{suffix}.{img_ext}")
|
||||
relative_path = os.path.relpath(preview_path, base_dir)
|
||||
preview_paths.append(relative_path.replace('\\', '/'))
|
||||
found_suffixes.append(suffix)
|
||||
break
|
||||
|
||||
@@ -349,16 +366,17 @@ async def api_get_lora_previews_list(request):
|
||||
if not found_suffixes:
|
||||
# Check root _power_preview directory
|
||||
for img_ext in ['jpg', 'jpeg', 'png']:
|
||||
preview_path = os.path.join(power_preview_dir, f"{lora_name}.{img_ext}")
|
||||
preview_path = os.path.join(power_preview_dir, f"{item_name}.{img_ext}")
|
||||
if path_exists(preview_path):
|
||||
preview_found = True
|
||||
preview_paths.append(f"_power_preview/{lora_name}.{img_ext}")
|
||||
relative_path = os.path.relpath(preview_path, base_dir)
|
||||
preview_paths.append(relative_path.replace('\\', '/'))
|
||||
break
|
||||
|
||||
# Check subfolders if not found in root
|
||||
if not preview_found:
|
||||
# Look for subfolder structure
|
||||
path_parts = lora_name.replace('/', os.sep).replace('\\', os.sep).split(os.sep)
|
||||
path_parts = item_name.replace('/', os.sep).replace('\\', os.sep).split(os.sep)
|
||||
if len(path_parts) > 1:
|
||||
subfolder = os.sep.join(path_parts[:-1])
|
||||
filename = path_parts[-1]
|
||||
@@ -369,13 +387,15 @@ async def api_get_lora_previews_list(request):
|
||||
preview_path = os.path.join(subfolder_path, f"{filename}.{img_ext}")
|
||||
if path_exists(preview_path):
|
||||
preview_found = True
|
||||
preview_paths.append(f"_power_preview/{subfolder}/{filename}.{img_ext}")
|
||||
relative_path = os.path.relpath(preview_path, base_dir)
|
||||
preview_paths.append(relative_path.replace('\\', '/'))
|
||||
break
|
||||
|
||||
if preview_found:
|
||||
api_response['previews'].append({
|
||||
'lora': lora_file,
|
||||
'preview_paths': preview_paths
|
||||
'lora': file_item, # Keep 'lora' key for backward compatibility
|
||||
'preview_paths': preview_paths,
|
||||
'type': 'model' if is_model_param else 'lora'
|
||||
})
|
||||
|
||||
return web.json_response(api_response)
|
||||
@@ -401,17 +421,26 @@ async def api_save_lora_preview_image(request):
|
||||
lora_name = post.get('lora_name')
|
||||
lora_path = post.get('lora_path', '')
|
||||
suffix = post.get('suffix', '')
|
||||
is_model = post.get('is_model', 'false').lower() == 'true'
|
||||
|
||||
if not image_file or not lora_name:
|
||||
api_response['status'] = '400'
|
||||
api_response['error'] = 'Missing required parameters: image and lora_name'
|
||||
return web.json_response(api_response)
|
||||
|
||||
# Get loras directory
|
||||
loras_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
# Get appropriate directory based on type
|
||||
if is_model:
|
||||
# For models, use checkpoints directory
|
||||
base_dir = folder_paths.get_folder_paths('checkpoints')[0]
|
||||
# Use _power_preview subdirectory for model previews
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
print(f"[Preview Image Debug] MODEL: Using directory {power_preview_dir}")
|
||||
else:
|
||||
# For LoRAs, use loras directory
|
||||
base_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
print(f"[Preview Image Debug] LORA: Using directory {power_preview_dir}")
|
||||
|
||||
# Create _power_preview directory structure
|
||||
power_preview_dir = os.path.join(loras_dir, '_power_preview')
|
||||
if lora_path:
|
||||
save_dir = os.path.join(power_preview_dir, lora_path)
|
||||
else:
|
||||
@@ -419,10 +448,13 @@ async def api_save_lora_preview_image(request):
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
print(f"[Preview Image Debug] Directory ensured: {save_dir}")
|
||||
print(f"[Preview Image Debug] Directory exists check: {os.path.exists(save_dir)}")
|
||||
|
||||
# Construct save path with optional suffix
|
||||
filename = f"{lora_name}{suffix}.jpg"
|
||||
save_path = os.path.join(save_dir, filename)
|
||||
print(f"[Preview Image Debug] Saving image to: {save_path}")
|
||||
|
||||
# Read image data
|
||||
image_data = image_file.file.read()
|
||||
@@ -446,9 +478,10 @@ async def api_save_lora_preview_image(request):
|
||||
# Save as JPEG with high quality
|
||||
img.save(save_path, 'JPEG', quality=90, optimize=True)
|
||||
|
||||
api_response['save_path'] = os.path.relpath(save_path, loras_dir)
|
||||
api_response['save_path'] = os.path.relpath(save_path, base_dir)
|
||||
api_response['file_size'] = os.path.getsize(save_path)
|
||||
api_response['filename'] = filename
|
||||
api_response['base_dir'] = base_dir
|
||||
|
||||
except Exception as e:
|
||||
api_response['status'] = '500'
|
||||
@@ -457,6 +490,128 @@ async def api_save_lora_preview_image(request):
|
||||
|
||||
return web.json_response(api_response)
|
||||
|
||||
@routes.post('/wanvid/api/model/preview-json')
|
||||
async def api_save_model_preview_json(request):
|
||||
"""Save JSON data for a model preview in the _power_preview/_model_preview directory."""
|
||||
|
||||
api_response = {'status': 200, 'message': 'Model preview JSON saved successfully'}
|
||||
|
||||
try:
|
||||
# Get form data
|
||||
post = await request.post()
|
||||
json_data = post.get('json')
|
||||
model_name = post.get('model_name')
|
||||
model_path = post.get('model_path', '')
|
||||
|
||||
if not json_data or not model_name:
|
||||
api_response['status'] = '400'
|
||||
api_response['error'] = 'Missing required parameters: json and model_name'
|
||||
return web.json_response(api_response)
|
||||
|
||||
# Get checkpoints directory as base
|
||||
checkpoints_dir = folder_paths.get_folder_paths('checkpoints')[0]
|
||||
# Use _power_preview subdirectory for model previews
|
||||
power_preview_dir = os.path.join(checkpoints_dir, '_power_preview')
|
||||
print(f"[Model JSON Debug] Using directory: {power_preview_dir}")
|
||||
|
||||
if model_path:
|
||||
save_dir = os.path.join(power_preview_dir, model_path)
|
||||
else:
|
||||
save_dir = power_preview_dir
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
print(f"[Model JSON Debug] Directory ensured: {save_dir}")
|
||||
print(f"[Model JSON Debug] Directory exists check: {os.path.exists(save_dir)}")
|
||||
|
||||
# Parse and validate JSON data
|
||||
try:
|
||||
json_content = json.loads(json_data)
|
||||
except json.JSONDecodeError as e:
|
||||
api_response['status'] = '400'
|
||||
api_response['error'] = f'Invalid JSON format: {str(e)}'
|
||||
return web.json_response(api_response)
|
||||
|
||||
# Construct save path - the JSON filename should have a trailing underscore
|
||||
# to match the image naming convention (e.g., model_name_.json for model_name_01.jpg)
|
||||
json_filename = f"{model_name}_.json"
|
||||
save_path = os.path.join(save_dir, json_filename)
|
||||
|
||||
# Save JSON file
|
||||
with open(save_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_content, f, indent=2, ensure_ascii=False)
|
||||
|
||||
api_response['save_path'] = os.path.relpath(save_path, checkpoints_dir)
|
||||
api_response['filename'] = json_filename
|
||||
api_response['base_dir'] = checkpoints_dir
|
||||
|
||||
except Exception as e:
|
||||
api_response['status'] = '500'
|
||||
api_response['error'] = f'Failed to save model preview JSON: {str(e)}'
|
||||
print(f"[Model Preview JSON Error] {str(e)}")
|
||||
|
||||
return web.json_response(api_response)
|
||||
|
||||
|
||||
@routes.post('/wanvid/api/lora/preview-json')
|
||||
async def api_save_lora_preview_json(request):
|
||||
"""Save JSON data for a LoRA preview in the _power_preview directory."""
|
||||
|
||||
api_response = {'status': 200, 'message': 'LoRA preview JSON saved successfully'}
|
||||
|
||||
try:
|
||||
# Get form data
|
||||
post = await request.post()
|
||||
json_data = post.get('json')
|
||||
lora_name = post.get('lora_name')
|
||||
lora_path = post.get('lora_path', '')
|
||||
|
||||
if not json_data or not lora_name:
|
||||
api_response['status'] = '400'
|
||||
api_response['error'] = 'Missing required parameters: json and lora_name'
|
||||
return web.json_response(api_response)
|
||||
|
||||
# Get loras directory
|
||||
loras_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
# Use _power_preview directory for LoRA previews
|
||||
power_preview_dir = os.path.join(loras_dir, '_power_preview')
|
||||
|
||||
if lora_path:
|
||||
save_dir = os.path.join(power_preview_dir, lora_path)
|
||||
else:
|
||||
save_dir = power_preview_dir
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
# Parse and validate JSON data
|
||||
try:
|
||||
json_content = json.loads(json_data)
|
||||
except json.JSONDecodeError as e:
|
||||
api_response['status'] = '400'
|
||||
api_response['error'] = f'Invalid JSON format: {str(e)}'
|
||||
return web.json_response(api_response)
|
||||
|
||||
# Construct save path - the JSON filename should have a trailing underscore
|
||||
# to match the image naming convention (e.g., lora_name_.json for lora_name_01.jpg)
|
||||
json_filename = f"{lora_name}_.json"
|
||||
save_path = os.path.join(save_dir, json_filename)
|
||||
|
||||
# Save JSON file
|
||||
with open(save_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_content, f, indent=2, ensure_ascii=False)
|
||||
|
||||
api_response['save_path'] = os.path.relpath(save_path, loras_dir)
|
||||
api_response['filename'] = json_filename
|
||||
api_response['base_dir'] = loras_dir
|
||||
|
||||
except Exception as e:
|
||||
api_response['status'] = '500'
|
||||
api_response['error'] = f'Failed to save LoRA preview JSON: {str(e)}'
|
||||
print(f"[LoRA Preview JSON Error] {str(e)}")
|
||||
|
||||
return web.json_response(api_response)
|
||||
|
||||
|
||||
async def models_info_response(
|
||||
request, model_type, maybe_fetch_civitai=False, maybe_fetch_metadata=False
|
||||
|
||||
@@ -25,7 +25,7 @@ async def delete_model_info(
|
||||
if file_path is None:
|
||||
return
|
||||
if del_info:
|
||||
remove_path(get_info_file(file_path))
|
||||
remove_path(get_info_file(file_path, model_type))
|
||||
if del_civitai or del_metadata:
|
||||
file_hash = _get_sha256_hash(file_path)
|
||||
if del_civitai:
|
||||
@@ -46,20 +46,20 @@ def get_file_info(file: str, model_type):
|
||||
'path': file_path,
|
||||
'modified': os.path.getmtime(file_path) * 1000, # millis
|
||||
'imageLocal': f'/wanvid/api/{model_type}/img?file={file}' if get_img_file(file_path) else None,
|
||||
'hasInfoFile': get_info_file(file_path) is not None,
|
||||
'hasInfoFile': get_info_file(file_path, model_type) is not None,
|
||||
}
|
||||
|
||||
|
||||
def get_info_file(file_path: str, force=False):
|
||||
def get_info_file(file_path: str, model_type: str, force=False):
|
||||
""" Returns the path to the info file.
|
||||
Saves JSON files with lora name in _power_preview folder inside loras.
|
||||
Saves JSON files with lora name in _power_preview folder inside the model_type folder.
|
||||
"""
|
||||
loras_dir = folder_paths.get_folder_paths('loras')[0]
|
||||
base_dir = folder_paths.get_folder_paths(model_type)[0]
|
||||
|
||||
# Create a mirrored folder structure in loras/_power_preview
|
||||
power_preview_dir = os.path.join(loras_dir, '_power_preview')
|
||||
power_preview_dir = os.path.join(base_dir, '_power_preview')
|
||||
|
||||
relative_path = os.path.relpath(file_path, loras_dir)
|
||||
relative_path = os.path.relpath(file_path, base_dir)
|
||||
# Get the filename without extension and change to .json
|
||||
filename_without_ext = os.path.splitext(os.path.basename(relative_path))[0]
|
||||
relative_dir = os.path.dirname(relative_path)
|
||||
@@ -71,7 +71,7 @@ def get_info_file(file_path: str, force=False):
|
||||
|
||||
# Create the directory if it doesn't exist
|
||||
info_dir = os.path.dirname(info_path)
|
||||
if not os.path.exists(info_dir):
|
||||
if force and not os.path.exists(info_dir):
|
||||
os.makedirs(info_dir)
|
||||
|
||||
return info_path if file_exists(info_path) or force else None
|
||||
@@ -89,7 +89,7 @@ def get_model_info_file_data(file: str, model_type, default=None):
|
||||
file_path = get_folder_path(file, model_type)
|
||||
if file_path is None:
|
||||
return default
|
||||
return load_json_file(get_info_file(file_path), default=default)
|
||||
return load_json_file(get_info_file(file_path, model_type), default=default)
|
||||
|
||||
|
||||
async def get_model_info(
|
||||
@@ -469,5 +469,5 @@ def save_model_info(file: str, info_data, model_type):
|
||||
file_path = get_folder_path(file, model_type)
|
||||
if file_path is None:
|
||||
return
|
||||
info_path = get_info_file(file_path, force=True)
|
||||
info_path = get_info_file(file_path, model_type, force=True)
|
||||
save_json_file(info_path, info_data)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -477,14 +477,19 @@ class WanInfoDialog extends WanDialog {
|
||||
|
||||
// Import and use the preview generation function
|
||||
const { generatePreviewFromFirstImage } = await import('./image_utils.js');
|
||||
const previewResult = await generatePreviewFromFirstImage(this.modelInfo, loraName, loraPath);
|
||||
console.log(`[DialogInfo] Preview image generated after refresh for ${loraName}:`, previewResult);
|
||||
// Determine item type from dialog instance or default to 'loras'
|
||||
const itemType = this.itemType || 'loras';
|
||||
console.log(`[DialogInfo Debug] Refresh preview generation - itemType: ${itemType}, loraName: ${loraName}, loraPath: ${loraPath}`);
|
||||
console.log(`[DialogInfo Debug] this.itemType: ${this.itemType}`);
|
||||
const previewResult = await generatePreviewFromFirstImage(this.modelInfo, loraName, loraPath, itemType);
|
||||
console.log(`[DialogInfo] Preview image generated after refresh for ${loraName} (${itemType}):`, previewResult);
|
||||
|
||||
// Add to success message
|
||||
const itemTypeName = itemType === 'checkpoints' ? 'Model' : 'LoRA';
|
||||
rgthree.showMessage({
|
||||
id: "refresh-info-" + generateId(4),
|
||||
type: "success",
|
||||
message: "LoRA information refreshed successfully (preview image generated)",
|
||||
message: `${itemTypeName} information refreshed successfully (preview image generated)`,
|
||||
timeout: 3000,
|
||||
});
|
||||
} catch (previewError) {
|
||||
@@ -532,8 +537,12 @@ class WanInfoDialog extends WanDialog {
|
||||
|
||||
// Import and use the preview generation function
|
||||
const { generatePreviewFromFirstImage } = await import('./image_utils.js');
|
||||
const previewResult = await generatePreviewFromFirstImage(this.modelInfo, loraName, loraPath);
|
||||
console.log(`[DialogInfo] Preview image generated after Civitai fetch for ${loraName}:`, previewResult);
|
||||
// Determine item type from dialog instance or default to 'loras'
|
||||
const itemType = this.itemType || 'loras';
|
||||
console.log(`[DialogInfo Debug] Civitai preview generation - itemType: ${itemType}, loraName: ${loraName}, loraPath: ${loraPath}`);
|
||||
console.log(`[DialogInfo Debug] this.itemType: ${this.itemType}`);
|
||||
const previewResult = await generatePreviewFromFirstImage(this.modelInfo, loraName, loraPath, itemType);
|
||||
console.log(`[DialogInfo] Preview image generated after Civitai fetch for ${loraName} (${itemType}):`, previewResult);
|
||||
} catch (previewError) {
|
||||
console.warn(`[DialogInfo] Failed to generate preview image for ${info.file}:`, previewError);
|
||||
}
|
||||
|
||||
@@ -150,18 +150,28 @@ export class ImageUtils {
|
||||
* @param {Blob} imageBlob - Processed image blob
|
||||
* @param {string} loraPath - Optional relative path within loras directory
|
||||
* @param {string} suffix - Optional suffix for filename (e.g., '_01', '_02')
|
||||
* @param {string} type - Type of item ('loras' or 'checkpoints')
|
||||
* @returns {Promise<Object>} - API response
|
||||
*/
|
||||
static async savePreviewImage(loraName, imageBlob, loraPath = '', suffix = '') {
|
||||
static async savePreviewImage(loraName, imageBlob, loraPath = '', suffix = '', type = 'loras') {
|
||||
const formData = new FormData();
|
||||
|
||||
// Create a unique filename with optional suffix
|
||||
const filename = `${loraName}${suffix}.jpg`;
|
||||
|
||||
formData.append('image', imageBlob, filename);
|
||||
formData.append('lora_name', loraName);
|
||||
formData.append('lora_path', loraPath);
|
||||
if (type === 'checkpoints') {
|
||||
formData.append('model_name', loraName);
|
||||
formData.append('model_path', loraPath);
|
||||
// Also include lora_name for compatibility
|
||||
formData.append('lora_name', loraName);
|
||||
formData.append('lora_path', loraPath);
|
||||
} else {
|
||||
formData.append('lora_name', loraName);
|
||||
formData.append('lora_path', loraPath);
|
||||
}
|
||||
formData.append('suffix', suffix);
|
||||
formData.append('is_model', type === 'checkpoints' ? 'true' : 'false');
|
||||
|
||||
try {
|
||||
const response = await fetch('/wanvid/api/lora/preview-image', {
|
||||
@@ -185,12 +195,13 @@ export class ImageUtils {
|
||||
* @param {Object} mediaInfo - Media object from dialog info
|
||||
* @param {string} loraName - Name of the LoRA
|
||||
* @param {string} loraPath - Optional relative path within loras directory
|
||||
* @param {string} type - Type of item ('loras' or 'checkpoints')
|
||||
* @returns {Promise<Object>} - Result of the operation
|
||||
*/
|
||||
static async generateAndSavePreview(mediaInfo, loraName, loraPath = '') {
|
||||
static async generateAndSavePreview(mediaInfo, loraName, loraPath = '', type = 'loras') {
|
||||
try {
|
||||
const frameInfo = mediaInfo.type === 'video' ? 'middle frame' : 'image';
|
||||
console.log(`[ImageUtils] Generating preview for ${loraName} from ${mediaInfo.type} (${frameInfo})`);
|
||||
console.log(`[ImageUtils] Generating preview for ${loraName} (${type}) from ${mediaInfo.type} (${frameInfo})`);
|
||||
|
||||
// Validate inputs
|
||||
if (!mediaInfo || !mediaInfo.url) {
|
||||
@@ -210,13 +221,13 @@ export class ImageUtils {
|
||||
}
|
||||
|
||||
// Save through backend API
|
||||
const result = await this.savePreviewImage(loraName, imageBlob, loraPath);
|
||||
const result = await this.savePreviewImage(loraName, imageBlob, loraPath, '', type);
|
||||
|
||||
console.log(`[ImageUtils] Preview saved successfully:`, result);
|
||||
console.log(`[ImageUtils] Preview saved successfully for ${loraName} (${type}):`, result);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[ImageUtils] Failed to generate preview for ${loraName}:`, error);
|
||||
console.error(`[ImageUtils] Failed to generate preview for ${loraName} (${type}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -227,9 +238,10 @@ export class ImageUtils {
|
||||
* @param {string} loraName - Name of the LoRA
|
||||
* @param {string} loraPath - Optional relative path within loras directory
|
||||
* @param {number} maxImages - Maximum number of preview images to generate
|
||||
* @param {string} type - Type of item ('loras' or 'checkpoints')
|
||||
* @returns {Promise<Array>} - Array of results for each generated preview
|
||||
*/
|
||||
static async generateMultiplePreviews(info, loraName, loraPath = '', maxImages = 3) {
|
||||
static async generateMultiplePreviews(info, loraName, loraPath = '', maxImages = 3, type = 'loras') {
|
||||
try {
|
||||
if (!info || !info.images || !info.images.length) {
|
||||
throw new Error('No images available in info data');
|
||||
@@ -262,7 +274,7 @@ export class ImageUtils {
|
||||
}
|
||||
|
||||
// Save with suffix
|
||||
const result = await this.savePreviewImage(loraName, imageBlob, loraPath, suffix);
|
||||
const result = await this.savePreviewImage(loraName, imageBlob, loraPath, suffix, type);
|
||||
result.suffix = suffix;
|
||||
results.push(result);
|
||||
|
||||
@@ -293,14 +305,15 @@ export class ImageUtils {
|
||||
* @param {Object} info - Dialog info object containing images array
|
||||
* @param {string} loraName - Name of the LoRA
|
||||
* @param {string} loraPath - Optional relative path within loras directory
|
||||
* @param {string} type - Type of item ('loras' or 'checkpoints')
|
||||
* @returns {Promise<Object>} - Result of the operation
|
||||
*/
|
||||
export async function generatePreviewFromFirstImage(info, loraName, loraPath = '') {
|
||||
export async function generatePreviewFromFirstImage(info, loraName, loraPath = '', type = 'loras') {
|
||||
if (!info || !info.images || !info.images.length) {
|
||||
throw new Error('No images available in info data');
|
||||
}
|
||||
|
||||
return ImageUtils.generateMultiplePreviews(info, loraName, loraPath, 3);
|
||||
return ImageUtils.generateMultiplePreviews(info, loraName, loraPath, 3, type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { moveArrayItem, removeArrayItem } from "./widgets.js";
|
||||
import { batchFetchService } from "./batch_fetch_service.js";
|
||||
import { renameLoRAFiles } from "./rename_utils.js";
|
||||
|
||||
// Function to display custom HTML context menu for LoRA widget actions
|
||||
export function showCustomLoraMenu(event, widget, node, position) {
|
||||
@@ -93,6 +95,29 @@ export function showCustomLoraMenu(event, widget, node, position) {
|
||||
return separator;
|
||||
};
|
||||
|
||||
// Function to show LoRA in Windows Explorer
|
||||
const showInExplorer = (loraName) => {
|
||||
// Use the ComfyUI API to open Explorer for the LoRA file
|
||||
fetch('/wanvideo_wrapper/open_explorer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ lora_name: loraName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Explorer opened successfully for:', loraName);
|
||||
} else {
|
||||
console.error('Failed to open Explorer:', data.error || 'Unknown error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error opening Explorer:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// Add menu items
|
||||
menu.appendChild(createMenuItem('ℹ️', 'Show Info', () => {
|
||||
// For the main LoRA widget context menu, always show the high variant info
|
||||
@@ -130,10 +155,15 @@ export function showCustomLoraMenu(event, widget, node, position) {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}, !canMoveDown));
|
||||
|
||||
menu.appendChild(createMenuItem('✏️', 'Rename', () => {
|
||||
const loraName = widget.value.lora_name;
|
||||
renameLoRAFiles(loraName, 'high', null, widget.callback, node, widget);
|
||||
}));
|
||||
|
||||
menu.appendChild(createMenuItem('🗑️', 'Remove', () => {
|
||||
removeArrayItem(node.widgets, widget);
|
||||
const computed = node.computeSize();
|
||||
node.size[1] = Math.max(node._tempHeight || 15, computed[1]);
|
||||
node.size[1] = Math.max(node._tempHeight || 15, computed[1]);
|
||||
node.setDirtyCanvas(true, true);
|
||||
}));
|
||||
|
||||
@@ -239,7 +269,7 @@ export function getLoraSlotMenuOptions(slot, event) {
|
||||
|
||||
// Use a slight delay to ensure the menu shows after the event completes
|
||||
setTimeout(() => {
|
||||
showLowVariantMenu(event, slot.widget, { x, y });
|
||||
showLowVariantMenu(event, slot.widget, this, { x, y });
|
||||
}, 10);
|
||||
|
||||
// Return false to prevent LiteGraph from showing its default menu
|
||||
@@ -292,7 +322,7 @@ export function getLoraSlotMenuOptions(slot, event) {
|
||||
}
|
||||
|
||||
// Function to display a simple context menu for low variant information
|
||||
function showLowVariantMenu(event, widget, position) {
|
||||
function showLowVariantMenu(event, widget, node, position) {
|
||||
console.log("showLowVariantMenu called.");
|
||||
// Remove any existing menu to prevent duplicates
|
||||
const existingMenu = document.getElementById('low-variant-menu');
|
||||
@@ -405,6 +435,19 @@ function showLowVariantMenu(event, widget, position) {
|
||||
// Add separator
|
||||
menu.appendChild(createSeparator());
|
||||
|
||||
// Add Toggle On/Off option
|
||||
menu.appendChild(createMenuItem(
|
||||
widget.value.low_active ? '⚫' : '🟢',
|
||||
widget.value.low_active ? 'Toggle Off' : 'Toggle On',
|
||||
() => {
|
||||
widget.value.low_active = !widget.value.low_active;
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
));
|
||||
|
||||
// Add separator
|
||||
menu.appendChild(createSeparator());
|
||||
|
||||
// Add Show Info option
|
||||
menu.appendChild(createMenuItem('ℹ️', 'Show Info', () => {
|
||||
// Show info for the low variant LoRA
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
// Import batch fetch service
|
||||
import { batchFetchService } from "./batch_fetch_service.js";
|
||||
import { renameLoRAFiles } from "./rename_utils.js";
|
||||
import { CHECKPOINT_INFO_SERVICE } from "./model_info_service.js";
|
||||
|
||||
// Function to show context menu for LoRA list items
|
||||
function showLoraListContextMenu(event, loraName) {
|
||||
console.log("[LoraPickerDialog] Opening context menu for:", loraName); // Debug log
|
||||
@@ -73,9 +78,82 @@ function showLoraListContextMenu(event, loraName) {
|
||||
return item;
|
||||
};
|
||||
|
||||
// Function to show LoRA in Windows Explorer
|
||||
const showInExplorer = (loraName) => {
|
||||
// Use the ComfyUI API to open Explorer for the LoRA file
|
||||
fetch('/wanvideo_wrapper/open_explorer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ lora_name: loraName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Explorer opened successfully for:', loraName);
|
||||
} else {
|
||||
console.error('Failed to open Explorer:', data.error || 'Unknown error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error opening Explorer:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if we're in model mode by accessing the dialog instance
|
||||
const dialogElement = document.querySelector('.lora-picker-dialog');
|
||||
const dialogInstance = dialogElement && dialogElement.dialogInstance;
|
||||
const isModelMode = dialogInstance && dialogInstance.options && dialogInstance.options.isModelMode;
|
||||
|
||||
// Add menu items
|
||||
menu.appendChild(createMenuItem('ℹ️', 'Show Info', () => {
|
||||
showLoraInfoDialog(loraName);
|
||||
if (isModelMode) {
|
||||
showModelInfoDialog(loraName);
|
||||
} else {
|
||||
showLoraInfoDialog(loraName);
|
||||
}
|
||||
}));
|
||||
|
||||
menu.appendChild(createMenuItem('📁', 'Show in Explorer', () => {
|
||||
if (isModelMode) {
|
||||
// For models, use the same API but the function is in power_model_loader_v2.js
|
||||
fetch('/wanvideo_wrapper/open_explorer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ lora_name: loraName }) // API expects lora_name but works for models too
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Explorer opened successfully for model:', loraName);
|
||||
} else {
|
||||
console.error('Failed to open Explorer for model:', data.error || 'Unknown error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error opening Explorer for model:', error);
|
||||
});
|
||||
} else {
|
||||
showInExplorer(loraName);
|
||||
}
|
||||
}));
|
||||
|
||||
menu.appendChild(createMenuItem('✏️', 'Rename', () => {
|
||||
// Use the same dialog instance to create refresh callback
|
||||
const refreshCallback = (dialogInstance && dialogInstance.refreshCallback)
|
||||
? dialogInstance.refreshCallback
|
||||
: null;
|
||||
|
||||
if (isModelMode) {
|
||||
// For models, we can use renameLoRAFiles with a different variant name
|
||||
// or create a model-specific rename function if needed
|
||||
renameLoRAFiles(loraName, 'model', refreshCallback);
|
||||
} else {
|
||||
renameLoRAFiles(loraName, 'high', refreshCallback);
|
||||
}
|
||||
}));
|
||||
|
||||
document.body.appendChild(menu);
|
||||
@@ -107,6 +185,9 @@ function showLoraInfoDialog(loraName) {
|
||||
// Import and show the LoRA info dialog
|
||||
import("./dialog_info.js").then(({ WanLoraInfoDialog }) => {
|
||||
const infoDialog = new WanLoraInfoDialog(loraName).show();
|
||||
// Store the item type for use in preview generation
|
||||
infoDialog.itemType = 'loras';
|
||||
console.log(`[LoraPickerDialog Debug] Created LoRA dialog with itemType: ${infoDialog.itemType}`);
|
||||
infoDialog.addEventListener("close", ((e) => {
|
||||
if (e.detail.dirty) {
|
||||
// Dialog was modified, could trigger refresh if needed
|
||||
@@ -123,6 +204,87 @@ File: ${loraName}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to show Model info dialog using the same simple approach as LoRA widgets
|
||||
function showModelInfoDialog(modelName) {
|
||||
if (!modelName || modelName === "None") {
|
||||
app.ui.dialog.show("No model selected");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[LoraPickerDialog] Opening info dialog for model: ${modelName}`);
|
||||
|
||||
// Import the dialog dynamically and create a model-specific info dialog
|
||||
import("./dialog_info.js").then(({ WanLoraInfoDialog }) => {
|
||||
// Create a model-specific info dialog by extending WanLoraInfoDialog
|
||||
class WanModelInfoDialog extends WanLoraInfoDialog {
|
||||
async getModelInfo(file) {
|
||||
console.log(`[LoraPickerDialog] Fetching info for model: ${file}`);
|
||||
try {
|
||||
const info = await CHECKPOINT_INFO_SERVICE.getInfo(file, false, false);
|
||||
console.log(`[LoraPickerDialog] Model info received:`, info);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error(`[LoraPickerDialog] Error fetching model info:`, error);
|
||||
// Fallback to basic info
|
||||
return {
|
||||
file: file,
|
||||
name: file,
|
||||
info: {},
|
||||
mtime: Date.now() / 1000
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Override refreshModelInfo to use CHECKPOINT_INFO_SERVICE instead of LORA_INFO_SERVICE
|
||||
async refreshModelInfo(file) {
|
||||
console.log(`[LoraPickerDialog] Refreshing model info for: ${file}`);
|
||||
console.log(`[LoraPickerDialog] Using CHECKPOINT_INFO_SERVICE for Civitai fetch`);
|
||||
try {
|
||||
const info = await CHECKPOINT_INFO_SERVICE.refreshInfo(file);
|
||||
console.log(`[LoraPickerDialog] Model info refreshed from Civitai:`, info);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error(`[LoraPickerDialog] Error refreshing model info from Civitai:`, error);
|
||||
// Return existing info on error instead of failing
|
||||
return this.modelInfo || {
|
||||
file: file,
|
||||
name: file,
|
||||
info: {},
|
||||
mtime: Date.now() / 1000
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Override the save method to use CHECKPOINT_INFO_SERVICE
|
||||
async savePartialInfo(info, fieldName, newValue) {
|
||||
console.log(`[LoraPickerDialog] Saving model info: ${fieldName} = ${newValue}`);
|
||||
try {
|
||||
await CHECKPOINT_INFO_SERVICE.savePartialInfo(info.file, { [fieldName]: newValue });
|
||||
console.log(`[LoraPickerDialog] Model info saved successfully`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[LoraPickerDialog] Error saving model info:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and show the dialog
|
||||
const dialog = new WanModelInfoDialog(modelName);
|
||||
// Store the item type for use in preview generation
|
||||
dialog.itemType = 'checkpoints';
|
||||
console.log(`[LoraPickerDialog Debug] Created model dialog with itemType: ${dialog.itemType}`);
|
||||
dialog.show();
|
||||
}).catch(error => {
|
||||
// Fallback to simple alert if dialog fails to load
|
||||
console.error("Failed to load model info dialog:", error);
|
||||
const modelInfo = `Model Information:\n\nFile: ${modelName}`;
|
||||
alert(modelInfo);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.lora-picker-dialog {
|
||||
@@ -617,7 +779,9 @@ export class LoraPickerDialog {
|
||||
this.currentPreviewLora = null;
|
||||
this.previewAvailabilityCache = new Set();
|
||||
this.folders = this.extractFolders();
|
||||
this.loadPreviewAvailability();
|
||||
|
||||
// Note: Don't call loadPreviewAvailability() here - it will be called in show() method
|
||||
// This prevents race conditions and ensures proper async loading
|
||||
}
|
||||
|
||||
show() {
|
||||
@@ -721,6 +885,9 @@ export class LoraPickerDialog {
|
||||
// Add to body
|
||||
document.body.appendChild(this.element);
|
||||
|
||||
// Store reference to dialog instance for access by rename function
|
||||
this.element.dialogInstance = this;
|
||||
|
||||
// Position preview area relative to dialog
|
||||
this.updatePreviewPosition();
|
||||
|
||||
@@ -741,6 +908,10 @@ export class LoraPickerDialog {
|
||||
this.previewArea.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading preview availability:', error);
|
||||
// Still render list even if preview loading fails
|
||||
this.renderList();
|
||||
});
|
||||
|
||||
// Focus on the search input automatically
|
||||
@@ -891,8 +1062,23 @@ export class LoraPickerDialog {
|
||||
refreshContainer.addEventListener("click", () => {
|
||||
if (this.refreshCallback) {
|
||||
this.refreshCallback();
|
||||
// After refresh, reload preview availability to update blue dots
|
||||
this.loadPreviewAvailability().then(() => {
|
||||
// Re-render the list to show updated blue indicators
|
||||
this.renderList();
|
||||
}).catch(error => {
|
||||
console.error('Error refreshing preview availability:', error);
|
||||
// Still render list even if preview refresh fails
|
||||
this.renderList();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add context menu for refresh button
|
||||
refreshContainer.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
this.showRefreshContextMenu(e);
|
||||
});
|
||||
|
||||
header.appendChild(refreshContainer);
|
||||
|
||||
@@ -1245,7 +1431,14 @@ export class LoraPickerDialog {
|
||||
// Add hover event handlers for preview
|
||||
item.addEventListener("mouseenter", () => {
|
||||
if (this.eyeRefreshState) {
|
||||
this.showPreview(name);
|
||||
if (this.hasPreview(name)) {
|
||||
this.showPreview(name);
|
||||
} else {
|
||||
this.hidePreview();
|
||||
}
|
||||
} else {
|
||||
// Also hide preview if eye refresh is OFF
|
||||
this.hidePreview();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1375,6 +1568,10 @@ export class LoraPickerDialog {
|
||||
}
|
||||
|
||||
showPreview(loraName) {
|
||||
if (!this.hasPreview(loraName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're already showing preview for this LoRA, don't do anything
|
||||
if (this.currentPreviewLora === loraName) {
|
||||
return;
|
||||
@@ -1525,51 +1722,47 @@ export class LoraPickerDialog {
|
||||
}
|
||||
}
|
||||
|
||||
loadSinglePreviewImage(filename, subfolder, suffix) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Build the API URL
|
||||
const apiUrl = new URL('/wanvid/api/loras/preview', window.location.origin);
|
||||
apiUrl.searchParams.set('file', filename);
|
||||
if (subfolder) {
|
||||
apiUrl.searchParams.set('subfolder', subfolder);
|
||||
}
|
||||
if (suffix) {
|
||||
apiUrl.searchParams.set('suffix', suffix);
|
||||
}
|
||||
async loadSinglePreviewImage(filename, subfolder, suffix) {
|
||||
try {
|
||||
// Determine item type based on options
|
||||
const itemType = this.options && this.options.itemType ? this.options.itemType :
|
||||
(this.options && this.options.isModelMode ? 'checkpoints' : 'loras');
|
||||
|
||||
const imagePath = apiUrl.pathname + apiUrl.search;
|
||||
// Use unified system from pull_info.js
|
||||
const { modelDataManager } = await import('./pull_info.js');
|
||||
const imageUrl = modelDataManager.getPreviewImageUrl(filename, itemType, suffix, subfolder);
|
||||
|
||||
// Create a new image object to test loading
|
||||
const testImage = new Image();
|
||||
|
||||
testImage.onload = () => {
|
||||
resolve(imagePath);
|
||||
};
|
||||
|
||||
testImage.onerror = () => {
|
||||
reject(new Error(`Failed to load image: ${imagePath}`));
|
||||
};
|
||||
|
||||
// Start loading the image
|
||||
testImage.src = imagePath;
|
||||
});
|
||||
// Test if the image loads successfully
|
||||
return new Promise((resolve, reject) => {
|
||||
const testImage = new Image();
|
||||
testImage.onload = () => resolve(imageUrl);
|
||||
testImage.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
|
||||
testImage.src = imageUrl;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Error building preview URL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPreviewAvailability() {
|
||||
// Determine item type based on options
|
||||
const itemType = this.options && this.options.itemType ? this.options.itemType :
|
||||
(this.options && this.options.isModelMode ? 'checkpoints' : 'loras');
|
||||
|
||||
// Use unified system from pull_info.js
|
||||
try {
|
||||
const response = await fetch('/wanvid/api/loras/previews');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.status === 200 && data.previews) {
|
||||
this.previewAvailabilityCache.clear();
|
||||
data.previews.forEach(preview => {
|
||||
this.previewAvailabilityCache.add(preview.lora);
|
||||
});
|
||||
}
|
||||
const { modelDataManager } = await import('./pull_info.js');
|
||||
const previewAvailability = await modelDataManager.getPreviewAvailability(itemType);
|
||||
|
||||
// Update our cache
|
||||
this.previewAvailabilityCache.clear();
|
||||
previewAvailability.forEach(itemName => {
|
||||
this.previewAvailabilityCache.add(itemName);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error loading preview availability:', error);
|
||||
// Don't clear cache on error, keep existing data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error loading preview availability:', error);
|
||||
}
|
||||
}
|
||||
|
||||
hasPreview(loraName) {
|
||||
@@ -1578,45 +1771,18 @@ export class LoraPickerDialog {
|
||||
|
||||
async loadPreviewJsonData(filename, subfolder) {
|
||||
try {
|
||||
// The JSON file keeps the trailing underscore from the base filename
|
||||
// So if images are: filename_01.jpg, filename_02.jpg
|
||||
// The JSON is: filename_.json (with trailing underscore)
|
||||
const jsonFilename = filename + '.json';
|
||||
// Determine item type based on options
|
||||
const itemType = this.options && this.options.itemType ? this.options.itemType :
|
||||
(this.options && this.options.isModelMode ? 'checkpoints' : 'loras');
|
||||
|
||||
// Try different approaches to access JSON files
|
||||
// The modified /wanvid/api/loras/preview endpoint now supports JSON files!
|
||||
const attempts = [
|
||||
// Method 1: Try the modified preview endpoint that now supports JSON (highest priority)
|
||||
`/wanvid/api/loras/preview?file=${jsonFilename}${subfolder ? '&subfolder=' + subfolder : ''}`,
|
||||
// Method 2: Standard ComfyUI API with correct _power_preview path
|
||||
`/api/view?filename=${jsonFilename}&subfolder=${subfolder}/_power_preview&type=models`,
|
||||
// Method 3: Alternative _power_preview path structure
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview/${subfolder}&type=models`,
|
||||
// Method 4: Try without subfolder in the filename
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`
|
||||
];
|
||||
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
const response = await fetch(attempt);
|
||||
if (response.ok) {
|
||||
const jsonData = await response.json();
|
||||
return jsonData;
|
||||
}
|
||||
} catch (attemptError) {
|
||||
// Continue to next attempt
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If no JSON data is found, return null - Copy+ buttons won't appear
|
||||
// This is graceful degradation - the feature works when JSON is available
|
||||
// but doesn't break when it's not
|
||||
// Use unified system from pull_info.js
|
||||
const { modelDataManager } = await import('./pull_info.js');
|
||||
const jsonData = await modelDataManager.getJsonData(filename, itemType, subfolder);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error loading JSON data:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
showCopyMessage(message, type = "success", copiedPrompt = null) {
|
||||
@@ -1728,4 +1894,359 @@ export class LoraPickerDialog {
|
||||
// Re-render the list
|
||||
this.renderList();
|
||||
}
|
||||
|
||||
showRefreshContextMenu(event) {
|
||||
// Remove any existing context menu
|
||||
const existingMenu = document.getElementById('refresh-context-menu');
|
||||
if (existingMenu) {
|
||||
existingMenu.remove();
|
||||
}
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.id = 'refresh-context-menu';
|
||||
menu.className = 'litegraph litecontextmenu litemenubar-panel';
|
||||
menu.style.cssText = `
|
||||
position: absolute !important;
|
||||
left: ${event.clientX}px !important;
|
||||
top: ${event.clientY}px !important;
|
||||
background-color: #1a1a1a !important;
|
||||
border: 1px solid #000 !important;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.8) !important;
|
||||
z-index: 10000 !important;
|
||||
padding: 4px !important;
|
||||
border-radius: 0px !important;
|
||||
min-width: 150px !important;
|
||||
display: block !important;
|
||||
font-family: Arial, sans-serif !important;
|
||||
font-size: 11px !important;
|
||||
`;
|
||||
|
||||
// Helper to create menu item
|
||||
const createMenuItem = (text, onClick, disabled = false) => {
|
||||
const item = document.createElement('div');
|
||||
item.style.cssText = `
|
||||
padding: 4px 8px !important;
|
||||
cursor: ${disabled ? 'default' : 'pointer'} !important;
|
||||
color: ${disabled ? '#555' : '#ddd'} !important;
|
||||
background-color: #1a1a1a !important;
|
||||
user-select: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
`;
|
||||
|
||||
if (!disabled) {
|
||||
item.onmouseover = () => {
|
||||
item.style.backgroundColor = '#2a2a2a';
|
||||
};
|
||||
item.onmouseout = () => {
|
||||
item.style.backgroundColor = '#1a1a1a';
|
||||
};
|
||||
item.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
menu.remove();
|
||||
document.removeEventListener('click', hideMenu);
|
||||
};
|
||||
}
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.textContent = text;
|
||||
|
||||
item.appendChild(textSpan);
|
||||
return item;
|
||||
};
|
||||
|
||||
// Add smart fetch menu items
|
||||
menu.appendChild(createMenuItem('📥 Fetch Missing', () => {
|
||||
this.startBatchFetch('missing');
|
||||
}));
|
||||
|
||||
menu.appendChild(createMenuItem('🔄 Update Existing', () => {
|
||||
this.startBatchFetch('existing');
|
||||
}));
|
||||
|
||||
menu.appendChild(createMenuItem('📥 Fetch All', () => {
|
||||
this.startBatchFetch('all');
|
||||
}));
|
||||
|
||||
// Add separator
|
||||
const separator = document.createElement('div');
|
||||
separator.style.cssText = `
|
||||
height: 1px !important;
|
||||
background-color: #333 !important;
|
||||
margin: 2px 0 !important;
|
||||
`;
|
||||
menu.appendChild(separator);
|
||||
|
||||
// Add "Refresh list" menu item
|
||||
menu.appendChild(createMenuItem('🔄 Refresh list', () => {
|
||||
if (this.refreshCallback) {
|
||||
this.refreshCallback();
|
||||
}
|
||||
}));
|
||||
|
||||
// Add "Show Stats" menu item
|
||||
menu.appendChild(createMenuItem('📊 Show Stats', async () => {
|
||||
// Show comprehensive LoRA statistics
|
||||
try {
|
||||
console.log('📊 Gathering LoRA statistics...');
|
||||
|
||||
// Get all available LoRA names from the system
|
||||
const allLoras = await fetch('/wanvid/api/loras')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Handle both direct array and wrapped response formats
|
||||
return Array.isArray(data) ? data : (data.loras || data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching LoRA list:', error);
|
||||
return [];
|
||||
});
|
||||
|
||||
// Get the preview callback function from the current dialog
|
||||
const hasPreviewCallback = (loraName) => {
|
||||
// Try to get the preview status from the dialog
|
||||
if (this.hasPreviewCallback) {
|
||||
return this.hasPreviewCallback(loraName);
|
||||
}
|
||||
|
||||
// Fallback: check if preview files exist via API
|
||||
return fetch(`/wanvid/api/loras/preview?file=${encodeURIComponent(loraName.replace(/\.[^/.]+$/, "") + '_01.jpg')}`)
|
||||
.then(response => response.ok)
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
// Gather comprehensive statistics
|
||||
const stats = await batchFetchService.gatherComprehensiveStats(hasPreviewCallback, allLoras);
|
||||
|
||||
// Display the stats dialog
|
||||
batchFetchService.createStatsDialog(stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error showing LoRA statistics:', error);
|
||||
|
||||
// Show error dialog
|
||||
const errorDialog = document.createElement('div');
|
||||
errorDialog.style.cssText = `
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
background: #2a2a2a !important;
|
||||
border: 2px solid #f44336 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 20px !important;
|
||||
z-index: 10000 !important;
|
||||
color: #fff !important;
|
||||
font-family: Arial, sans-serif !important;
|
||||
text-align: center !important;
|
||||
`;
|
||||
errorDialog.innerHTML = `
|
||||
<h3 style="color: #f44336; margin-top: 0;">❌ Error</h3>
|
||||
<p>Failed to gather LoRA statistics:</p>
|
||||
<p style="color: #ccc; font-size: 12px;">${error.message}</p>
|
||||
<button onclick="this.parentElement.remove()" style="
|
||||
background: #f44336 !important;
|
||||
border: none !important;
|
||||
color: white !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 4px !important;
|
||||
cursor: pointer !important;
|
||||
margin-top: 12px !important;
|
||||
">Close</button>
|
||||
`;
|
||||
document.body.appendChild(errorDialog);
|
||||
}
|
||||
}));
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Prevent menu close when interacting with menu
|
||||
menu.onclick = (e) => e.stopPropagation();
|
||||
|
||||
// Hide menu when clicking outside
|
||||
const hideMenu = (e) => {
|
||||
if (menu && !menu.contains(e.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', hideMenu);
|
||||
}
|
||||
};
|
||||
|
||||
// Add listeners after a short delay to prevent immediate closure
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', hideMenu);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start batch fetch operation with progress UI
|
||||
* @param {string} filterType - 'missing', 'existing', or 'all'
|
||||
*/
|
||||
async startBatchFetch(filterType) {
|
||||
if (!this.loras || this.loras.length === 0) {
|
||||
console.warn('No LoRAs available to fetch');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create progress UI
|
||||
const progressUI = this.createProgressUI(filterType);
|
||||
const { loadingOverlay, progressFill, progressText, cancelButton } = progressUI;
|
||||
|
||||
// Progress callback functions
|
||||
const progressCallbacks = {
|
||||
updateProgress: (percentage) => {
|
||||
progressFill.style.width = `${percentage}%`;
|
||||
},
|
||||
updateText: (text) => {
|
||||
progressText.textContent = text;
|
||||
},
|
||||
showError: (error) => {
|
||||
progressText.textContent = error;
|
||||
progressFill.style.backgroundColor = '#ff4444';
|
||||
},
|
||||
showCompletion: (message) => {
|
||||
progressText.textContent = message;
|
||||
},
|
||||
checkCancelled: () => {
|
||||
return batchFetchService.cancelFlag;
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel button handler
|
||||
cancelButton.onclick = () => {
|
||||
batchFetchService.cancel();
|
||||
cancelButton.textContent = 'Cancelling...';
|
||||
cancelButton.disabled = true;
|
||||
cancelButton.style.backgroundColor = '#6c757d';
|
||||
cancelButton.style.cursor = 'default';
|
||||
progressText.textContent = 'Cancelling operation...';
|
||||
};
|
||||
|
||||
try {
|
||||
// Start batch fetch using the service
|
||||
const result = await batchFetchService.fetchAllLoras(
|
||||
this.loras,
|
||||
filterType,
|
||||
this.hasPreview.bind(this),
|
||||
progressCallbacks
|
||||
);
|
||||
|
||||
// Wait to show completion
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Batch fetch error:', error);
|
||||
progressCallbacks.showError(`Error: ${error.message}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
} finally {
|
||||
// Clean up UI
|
||||
if (loadingOverlay.parentNode) {
|
||||
loadingOverlay.parentNode.removeChild(loadingOverlay);
|
||||
}
|
||||
|
||||
// Update preview availability cache and refresh
|
||||
try {
|
||||
await this.loadPreviewAvailability();
|
||||
console.log('Preview availability cache updated successfully');
|
||||
} catch (error) {
|
||||
console.warn('Failed to update preview availability cache:', error);
|
||||
}
|
||||
|
||||
// Refresh the LoRA list to show new previews
|
||||
if (this.refreshCallback) {
|
||||
this.refreshCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create progress UI for batch operations
|
||||
* @param {string} filterType - Type of operation being performed
|
||||
* @returns {Object} Progress UI elements
|
||||
*/
|
||||
createProgressUI(filterType) {
|
||||
const loadingOverlay = document.createElement('div');
|
||||
loadingOverlay.style.cssText = `
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background-color: rgba(0,0,0,0.8) !important;
|
||||
z-index: 10001 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
font-family: Arial, sans-serif !important;
|
||||
color: #ddd !important;
|
||||
`;
|
||||
|
||||
const loadingText = document.createElement('div');
|
||||
loadingText.style.cssText = `
|
||||
font-size: 18px !important;
|
||||
margin-bottom: 10px !important;
|
||||
`;
|
||||
loadingText.textContent = `Fetching ${filterType} Previews...`;
|
||||
|
||||
const progressText = document.createElement('div');
|
||||
progressText.style.cssText = `
|
||||
font-size: 14px !important;
|
||||
color: #aaa !important;
|
||||
`;
|
||||
progressText.textContent = 'Initializing...';
|
||||
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.style.cssText = `
|
||||
width: 300px !important;
|
||||
height: 20px !important;
|
||||
background-color: #333 !important;
|
||||
border: 1px solid #555 !important;
|
||||
border-radius: 10px !important;
|
||||
margin-top: 10px !important;
|
||||
overflow: hidden !important;
|
||||
`;
|
||||
|
||||
const progressFill = document.createElement('div');
|
||||
progressFill.style.cssText = `
|
||||
height: 100% !important;
|
||||
background-color: #007acc !important;
|
||||
width: 0% !important;
|
||||
transition: width 0.3s ease !important;
|
||||
`;
|
||||
|
||||
// Add cancel button
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.style.cssText = `
|
||||
margin-top: 15px !important;
|
||||
padding: 8px 16px !important;
|
||||
background-color: #dc3545 !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
border-radius: 4px !important;
|
||||
cursor: pointer !important;
|
||||
font-family: Arial, sans-serif !important;
|
||||
font-size: 14px !important;
|
||||
transition: background-color 0.2s ease !important;
|
||||
`;
|
||||
|
||||
cancelButton.onmouseover = () => {
|
||||
cancelButton.style.backgroundColor = '#c82333';
|
||||
};
|
||||
|
||||
cancelButton.onmouseout = () => {
|
||||
cancelButton.style.backgroundColor = '#dc3545';
|
||||
};
|
||||
|
||||
progressBar.appendChild(progressFill);
|
||||
loadingOverlay.appendChild(loadingText);
|
||||
loadingOverlay.appendChild(progressText);
|
||||
loadingOverlay.appendChild(progressBar);
|
||||
loadingOverlay.appendChild(cancelButton);
|
||||
document.body.appendChild(loadingOverlay);
|
||||
|
||||
return { loadingOverlay, progressFill, progressText, cancelButton };
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,63 +1,25 @@
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { rgthree } from "../rgthree/common/rgthree.js";
|
||||
import { rgthreeApi as wanvidApi } from "./rgthree_api.js";
|
||||
import { RgthreeBetterButtonWidget, RgthreeDividerWidget, CombinedOptionsWidget, PowerLoraLoaderHeaderWidget, PowerLoraLoaderWidget } from "./widgets.js";
|
||||
import { LoraPickerDialog } from "./lora_picker_dialog.js";
|
||||
import { getLoraSlotInPosition, getLoraSlotMenuOptions } from "./lora_context_menu.js";
|
||||
|
||||
let lorasCache = null;
|
||||
// Clear cache to ensure new format is used
|
||||
localStorage.removeItem("wanVideoPowerLoraLoader.lorasCache");
|
||||
import { modelDataManager, UIManager } from "./pull_info.js";
|
||||
|
||||
const MINIMUM_NODE_WIDTH = 480;
|
||||
|
||||
// Unified data fetching using pull_info.js
|
||||
async function getLoras(forceRefresh = false) {
|
||||
// Check if we have cached data in localStorage
|
||||
const cachedData = localStorage.getItem("wanVideoPowerLoraLoader.lorasCache");
|
||||
if (cachedData && lorasCache && !forceRefresh) {
|
||||
return lorasCache;
|
||||
}
|
||||
|
||||
// Clear cache if force refresh is requested
|
||||
if (forceRefresh) {
|
||||
lorasCache = null;
|
||||
localStorage.removeItem("wanVideoPowerLoraLoader.lorasCache");
|
||||
}
|
||||
|
||||
try {
|
||||
// Use rgthreeApi with forceRefresh parameter
|
||||
const lorasData = await wanvidApi.getLoras(forceRefresh);
|
||||
// Convert rgthreeApi format to our expected format
|
||||
lorasCache = lorasData.map(lora => {
|
||||
if (typeof lora === 'string') {
|
||||
return { name: lora, mtime: 0 };
|
||||
}
|
||||
return {
|
||||
name: lora.name || lora.filename || lora.file || JSON.stringify(lora),
|
||||
mtime: lora.mtime || lora.modified || lora.modified_time || 0
|
||||
};
|
||||
});
|
||||
|
||||
// Cache the data in localStorage
|
||||
localStorage.setItem("wanVideoPowerLoraLoader.lorasCache", JSON.stringify(lorasCache));
|
||||
|
||||
return lorasCache;
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to fetch loras:", error);
|
||||
return [];
|
||||
}
|
||||
return modelDataManager.getItems('loras', forceRefresh);
|
||||
}
|
||||
|
||||
// Functions to persist favorites
|
||||
// Functions to persist favorites using pull_info.js
|
||||
function saveFavorites(favorites) {
|
||||
localStorage.setItem("wanVideoPowerLoraLoader.favorites", JSON.stringify(favorites));
|
||||
UIManager.saveFavorites(favorites, 'loras');
|
||||
}
|
||||
|
||||
function loadFavorites() {
|
||||
const favorites = localStorage.getItem("wanVideoPowerLoraLoader.favorites");
|
||||
return favorites !== null ? JSON.parse(favorites) : [];
|
||||
return UIManager.loadFavorites('loras');
|
||||
}
|
||||
|
||||
function showLoraPicker(event, callback, node = null) {
|
||||
@@ -69,6 +31,7 @@ function showLoraPicker(event, callback, node = null) {
|
||||
sort: "Latest",
|
||||
favorites: savedFavorites,
|
||||
favoritesOnly: false,
|
||||
itemType: 'loras', // Explicitly set item type for unified system
|
||||
onFavoriteToggle: (loraName) => {
|
||||
const index = savedFavorites.indexOf(loraName);
|
||||
if (index !== -1) {
|
||||
@@ -78,16 +41,32 @@ function showLoraPicker(event, callback, node = null) {
|
||||
// Add to favorites
|
||||
savedFavorites.push(loraName);
|
||||
}
|
||||
// Save to localStorage
|
||||
// Save to localStorage using unified system
|
||||
saveFavorites(savedFavorites);
|
||||
// Update dialog's favorites
|
||||
dialog.options.favorites = savedFavorites;
|
||||
// Re-render the list to update star colors
|
||||
dialog.renderList();
|
||||
},
|
||||
refreshCallback: () => {
|
||||
// Bulk refresh removed - refresh individual LoRAs through their dialogs instead
|
||||
console.log('[WanVideoPowerLoraLoader] Individual LoRA refresh available in LoRA info dialog');
|
||||
refreshCallback: async () => {
|
||||
// Perform actual refresh of the LoRA list using unified system
|
||||
try {
|
||||
console.log('[WanVideoPowerLoraLoader] Refreshing LoRA list...');
|
||||
const freshLoras = await getLoras(true);
|
||||
dialog.updateLoras(freshLoras);
|
||||
|
||||
// Also refresh preview availability for blue dots using unified system
|
||||
console.log('[WanVideoPowerLoraLoader] Refreshing preview availability...');
|
||||
await modelDataManager.getPreviewAvailability('loras');
|
||||
|
||||
// Re-render the list to show updated blue dots
|
||||
console.log('[WanVideoPowerLoraLoader] Re-rendering list to show updated blue dots...');
|
||||
dialog.renderList();
|
||||
|
||||
console.log('[WanVideoPowerLoraLoader] LoRA list refreshed successfully');
|
||||
} catch (error) {
|
||||
console.error('[WanVideoPowerLoraLoader] Error refreshing LoRA list:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
@@ -325,12 +304,10 @@ app.registerExtension({
|
||||
|
||||
// Add refreshComboInNode method to handle R key press for reloading definitions
|
||||
nodeType.prototype.refreshComboInNode = function(defs) {
|
||||
// Use wanvidApi to refresh the LoRA list
|
||||
// Bulk refresh removed - just clear cache and refresh list
|
||||
lorasCache = null;
|
||||
localStorage.removeItem("wanVideoPowerLoraLoader.lorasCache");
|
||||
// Use unified system to refresh the LoRA list
|
||||
modelDataManager.clearCaches('loras');
|
||||
|
||||
// Fetch fresh loras and update cache
|
||||
// Fetch fresh loras and update cache
|
||||
getLoras(true).then((lorasDetails) => {
|
||||
nodeType.prototype.lorasCache = lorasDetails;
|
||||
|
||||
@@ -360,7 +337,9 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
// Trigger a value change to update the UI
|
||||
widget.callback(widget.value);
|
||||
if (widget.callback && typeof widget.callback === 'function') {
|
||||
widget.callback(widget.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +1,49 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { rgthree } from "../rgthree/common/rgthree.js";
|
||||
import { rgthreeApi as wanvidApi } from "./rgthree_api.js";
|
||||
import { RgthreeBetterButtonWidget, RgthreeDividerWidget, StrengthCopyWidget, PowerLoraLoaderHeaderWidget, PowerLoraLoaderWidget } from "./widgets.js";
|
||||
import { LoraPickerDialog } from "./lora_picker_dialog.js";
|
||||
import { getLoraSlotInPosition, getLoraSlotMenuOptions } from "./lora_context_menu.js";
|
||||
|
||||
let lorasCache = null;
|
||||
// Clear cache to ensure new format is used
|
||||
localStorage.removeItem("powerLoraLoaderV2.lorasCache");
|
||||
import { modelDataManager, UIManager } from "./pull_info.js";
|
||||
|
||||
const MINIMUM_NODE_WIDTH = 480;
|
||||
|
||||
// Unified data fetching using pull_info.js
|
||||
async function getLoras(forceRefresh = false) {
|
||||
// Check if we have cached data in localStorage
|
||||
const cachedData = localStorage.getItem("powerLoraLoaderV2.lorasCache");
|
||||
if (cachedData && lorasCache && !forceRefresh) {
|
||||
return lorasCache;
|
||||
}
|
||||
|
||||
// Clear cache if force refresh is requested
|
||||
if (forceRefresh) {
|
||||
lorasCache = null;
|
||||
localStorage.removeItem("powerLoraLoaderV2.lorasCache");
|
||||
}
|
||||
|
||||
try {
|
||||
// Use rgthreeApi with forceRefresh parameter
|
||||
const lorasData = await wanvidApi.getLoras(forceRefresh);
|
||||
// Convert rgthreeApi format to our expected format
|
||||
lorasCache = lorasData.map(lora => {
|
||||
if (typeof lora === 'string') {
|
||||
return { name: lora, mtime: 0 };
|
||||
}
|
||||
return {
|
||||
name: lora.name || lora.filename || lora.file || JSON.stringify(lora),
|
||||
mtime: lora.mtime || lora.modified || lora.modified_time || 0
|
||||
};
|
||||
});
|
||||
|
||||
// Cache the data in localStorage
|
||||
localStorage.setItem("powerLoraLoaderV2.lorasCache", JSON.stringify(lorasCache));
|
||||
|
||||
return lorasCache;
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to fetch loras:", error);
|
||||
return [];
|
||||
}
|
||||
return modelDataManager.getItems('loras', forceRefresh);
|
||||
}
|
||||
|
||||
// Functions to persist favorites
|
||||
// Functions to persist favorites using pull_info.js
|
||||
function saveFavorites(favorites) {
|
||||
localStorage.setItem("powerLoraLoaderV2.favorites", JSON.stringify(favorites));
|
||||
UIManager.saveFavorites(favorites, 'loras');
|
||||
}
|
||||
|
||||
function loadFavorites() {
|
||||
const favorites = localStorage.getItem("powerLoraLoaderV2.favorites");
|
||||
return favorites !== null ? JSON.parse(favorites) : [];
|
||||
return UIManager.loadFavorites('loras');
|
||||
}
|
||||
|
||||
// Functions to persist UI state
|
||||
// Functions to persist UI state using pull_info.js
|
||||
function saveFoldersVisible(foldersVisible) {
|
||||
localStorage.setItem("powerLoraLoaderV2.foldersVisible", JSON.stringify(foldersVisible));
|
||||
UIManager.saveUIProperty('foldersVisible', foldersVisible, 'loras');
|
||||
}
|
||||
|
||||
function loadFoldersVisible() {
|
||||
const foldersVisible = localStorage.getItem("powerLoraLoaderV2.foldersVisible");
|
||||
return foldersVisible !== null ? JSON.parse(foldersVisible) : false;
|
||||
return UIManager.loadUIProperty('foldersVisible', 'loras', false);
|
||||
}
|
||||
|
||||
function saveSelectedFolder(selectedFolder) {
|
||||
localStorage.setItem("powerLoraLoaderV2.selectedFolder", JSON.stringify(selectedFolder));
|
||||
UIManager.saveUIProperty('selectedFolder', selectedFolder, 'loras');
|
||||
}
|
||||
|
||||
function loadSelectedFolder() {
|
||||
const selectedFolder = localStorage.getItem("powerLoraLoaderV2.selectedFolder");
|
||||
return selectedFolder !== null ? JSON.parse(selectedFolder) : null;
|
||||
return UIManager.loadUIProperty('selectedFolder', 'loras', null);
|
||||
}
|
||||
|
||||
function saveEyeRefreshState(eyeRefreshState) {
|
||||
localStorage.setItem("powerLoraLoaderV2.eyeRefreshState", JSON.stringify(eyeRefreshState));
|
||||
UIManager.saveUIProperty('eyeRefreshState', eyeRefreshState, 'loras');
|
||||
}
|
||||
|
||||
function loadEyeRefreshState() {
|
||||
const eyeRefreshState = localStorage.getItem("powerLoraLoaderV2.eyeRefreshState");
|
||||
return eyeRefreshState !== null ? JSON.parse(eyeRefreshState) : false;
|
||||
return UIManager.loadUIProperty('eyeRefreshState', 'loras', false);
|
||||
}
|
||||
|
||||
function showLoraPicker(event, callback, node = null) {
|
||||
@@ -116,6 +75,7 @@ function showLoraPicker(event, callback, node = null) {
|
||||
sort: "Latest",
|
||||
favorites: savedFavorites,
|
||||
favoritesOnly: false,
|
||||
itemType: 'loras', // Explicitly set item type for unified system
|
||||
onFavoriteToggle: (loraName) => {
|
||||
const index = savedFavorites.indexOf(loraName);
|
||||
if (index !== -1) {
|
||||
@@ -125,7 +85,7 @@ function showLoraPicker(event, callback, node = null) {
|
||||
// Add to favorites
|
||||
savedFavorites.push(loraName);
|
||||
}
|
||||
// Save to localStorage
|
||||
// Save to localStorage using unified system
|
||||
saveFavorites(savedFavorites);
|
||||
// Update dialog's favorites
|
||||
dialog.options.favorites = savedFavorites;
|
||||
@@ -133,9 +93,25 @@ function showLoraPicker(event, callback, node = null) {
|
||||
dialog.renderList();
|
||||
},
|
||||
foldersVisible: foldersVisible,
|
||||
refreshCallback: () => {
|
||||
// Bulk refresh removed - refresh individual LoRAs through their dialogs instead
|
||||
console.log('[PowerLoraLoaderV2] Individual LoRA refresh available in LoRA info dialog');
|
||||
refreshCallback: async () => {
|
||||
// Perform actual refresh of the LoRA list using unified system
|
||||
try {
|
||||
console.log('[PowerLoraLoaderV2] Refreshing LoRA list...');
|
||||
const freshLoras = await getLoras(true);
|
||||
dialog.updateLoras(freshLoras);
|
||||
|
||||
// Also refresh preview availability for blue dots using unified system
|
||||
console.log('[PowerLoraLoaderV2] Refreshing preview availability...');
|
||||
await modelDataManager.getPreviewAvailability('loras');
|
||||
|
||||
// Re-render the list to show updated blue dots
|
||||
console.log('[PowerLoraLoaderV2] Re-rendering list to show updated blue dots...');
|
||||
dialog.renderList();
|
||||
|
||||
console.log('[PowerLoraLoaderV2] LoRA list refreshed successfully');
|
||||
} catch (error) {
|
||||
console.error('[PowerLoraLoaderV2] Error refreshing LoRA list:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -428,12 +404,10 @@ app.registerExtension({
|
||||
|
||||
// Add refreshComboInNode method to handle R key press for reloading definitions
|
||||
nodeType.prototype.refreshComboInNode = function(defs) {
|
||||
// Use wanvidApi to refresh the LoRA list
|
||||
// Bulk refresh removed - just clear cache and refresh list
|
||||
lorasCache = null;
|
||||
localStorage.removeItem("powerLoraLoaderV2.lorasCache");
|
||||
// Use unified system to refresh the LoRA list
|
||||
modelDataManager.clearCaches('loras');
|
||||
|
||||
// Fetch fresh loras and update cache
|
||||
// Fetch fresh loras and update cache
|
||||
getLoras(true).then((lorasDetails) => {
|
||||
nodeType.prototype.lorasCache = lorasDetails;
|
||||
|
||||
@@ -463,7 +437,9 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
// Trigger a value change to update the UI
|
||||
widget.callback(widget.value);
|
||||
if (widget.callback && typeof widget.callback === 'function') {
|
||||
widget.callback(widget.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { rgthree } from "../rgthree/common/rgthree.js";
|
||||
import { RgthreeBetterButtonWidget, RgthreeDividerWidget } from "./widgets.js";
|
||||
import { drawRoundedRectangle, drawTogglePart, drawWidgetButton, fitString, isLowQuality } from "./widgets.js";
|
||||
import { LoraPickerDialog } from "./lora_picker_dialog.js";
|
||||
import { WanLoraInfoDialog } from "./dialog_info.js";
|
||||
import { CHECKPOINT_INFO_SERVICE } from "./model_info_service.js";
|
||||
import { modelDataManager, UIManager } from "./pull_info.js";
|
||||
|
||||
const MINIMUM_NODE_WIDTH = 300;
|
||||
const MARGIN = 10;
|
||||
//const ICON_WIDTH = 24;
|
||||
|
||||
// Unified data fetching using pull_info.js
|
||||
async function getModels(forceRefresh = false) {
|
||||
return modelDataManager.getItems('checkpoints', forceRefresh);
|
||||
}
|
||||
|
||||
// Functions to persist favorites using pull_info.js
|
||||
function saveFavorites(favorites) {
|
||||
UIManager.saveFavorites(favorites, 'checkpoints');
|
||||
}
|
||||
|
||||
function loadFavorites() {
|
||||
return UIManager.loadFavorites('checkpoints');
|
||||
}
|
||||
|
||||
// Functions to persist UI state using pull_info.js
|
||||
function saveFoldersVisible(foldersVisible) {
|
||||
UIManager.saveUIProperty('foldersVisible', foldersVisible, 'checkpoints');
|
||||
}
|
||||
|
||||
function loadFoldersVisible() {
|
||||
return UIManager.loadUIProperty('foldersVisible', 'checkpoints', false);
|
||||
}
|
||||
|
||||
function saveSelectedFolder(selectedFolder) {
|
||||
UIManager.saveUIProperty('selectedFolder', selectedFolder, 'checkpoints');
|
||||
}
|
||||
|
||||
function loadSelectedFolder() {
|
||||
return UIManager.loadUIProperty('selectedFolder', 'checkpoints', null);
|
||||
}
|
||||
|
||||
function saveEyeRefreshState(eyeRefreshState) {
|
||||
UIManager.saveUIProperty('eyeRefreshState', eyeRefreshState, 'checkpoints');
|
||||
}
|
||||
|
||||
function loadEyeRefreshState() {
|
||||
return UIManager.loadUIProperty('eyeRefreshState', 'checkpoints', false);
|
||||
}
|
||||
|
||||
|
||||
// Custom widget for model selection
|
||||
class PowerModelLoaderWidget {
|
||||
constructor(name, showModelChooser) {
|
||||
this.name = name;
|
||||
this.type = "custom";
|
||||
this.showModelChooser = showModelChooser;
|
||||
this.value = { on: true, model: "None" };
|
||||
this.options = {};
|
||||
this.y = 0;
|
||||
this.last_y = 0;
|
||||
this.mouseDowned = null;
|
||||
this.isMouseDownedAndOver = false;
|
||||
this.hitAreas = {
|
||||
model: { bounds: [0, 0], onClick: this.onModelClick },
|
||||
};
|
||||
}
|
||||
|
||||
set value(v) {
|
||||
this._value = v;
|
||||
if (typeof this._value !== "object") {
|
||||
this._value = { on: true, model: "None" };
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
draw(ctx, node, w, y, h) {
|
||||
drawRoundedRectangle(ctx, [MARGIN, y], [w - MARGIN * 2, h], h * 0.5);
|
||||
|
||||
let posX = MARGIN + 10; // Add 10px offset from left
|
||||
|
||||
// Model name with bullet point
|
||||
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
|
||||
ctx.font = "14px Arial";
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "middle";
|
||||
const displayText = this.value.model || "None";
|
||||
const displayTextWithBullet = "• " + displayText;
|
||||
const fittedText = fitString(ctx, displayTextWithBullet, w - posX - MARGIN * 2);
|
||||
ctx.fillText(fittedText, posX, y + h / 2);
|
||||
|
||||
// Store hit area bounds for model selection
|
||||
this.hitAreas.model.bounds = [posX, y, w - posX - MARGIN * 2, h];
|
||||
}
|
||||
|
||||
onModelClick = (event, pos, node) => {
|
||||
this.showModelChooser(event, (model) => {
|
||||
if (model) {
|
||||
this.setModel(model);
|
||||
}
|
||||
}, node);
|
||||
return true;
|
||||
}
|
||||
|
||||
setModel(model) {
|
||||
this.value.model = model;
|
||||
if (this.callback) {
|
||||
this.callback(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
mouse(event, pos, node) {
|
||||
if (event.type === "pointerdown") {
|
||||
// Check if click is within model area
|
||||
if (this.clickWasWithinBounds(pos, this.hitAreas.model.bounds)) {
|
||||
// Only handle left-clicks for model selection
|
||||
if (event.button === 0 || event.button === 1) {
|
||||
this.onModelClick(event, pos, node);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
clickWasWithinBounds(pos, bounds) {
|
||||
if (!bounds) return false;
|
||||
return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[2] &&
|
||||
pos[1] >= bounds[1] && pos[1] <= bounds[1] + bounds[3];
|
||||
}
|
||||
|
||||
computeSize(width) {
|
||||
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
|
||||
}
|
||||
}
|
||||
|
||||
// Slot detection function for model widgets
|
||||
function getModelSlotInPosition(canvasX, canvasY) {
|
||||
const slot = LGraphNode.prototype.getSlotInPosition?.call(this, canvasX, canvasY);
|
||||
if (!slot) {
|
||||
// Check if click is on a model widget
|
||||
for (const widget of this.widgets || []) {
|
||||
if (!widget.last_y) continue;
|
||||
|
||||
// Check if click Y is within this widget's bounds
|
||||
const widgetTop = this.pos[1] + widget.last_y;
|
||||
const widgetBottom = widgetTop + LiteGraph.NODE_WIDGET_HEIGHT;
|
||||
|
||||
if (canvasY >= widgetTop && canvasY <= widgetBottom) {
|
||||
// Found the widget at this position
|
||||
if (widget.name?.startsWith("model_")) {
|
||||
// Return a virtual slot for the model widget
|
||||
return {
|
||||
input: null,
|
||||
output: {
|
||||
type: "MODEL_WIDGET",
|
||||
name: widget.name,
|
||||
widget: widget
|
||||
},
|
||||
pos: [this.pos[0], widgetTop],
|
||||
slot_index: -1 // Virtual slot
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Slot menu options for model widgets
|
||||
function getModelSlotMenuOptions(slot, event) {
|
||||
// Check if this is our model widget virtual slot
|
||||
if (slot?.output?.type === "MODEL_WIDGET") {
|
||||
const widget = slot.output.widget;
|
||||
const modelName = widget.value.model;
|
||||
|
||||
return [
|
||||
{
|
||||
content: "Show Info",
|
||||
callback: () => {
|
||||
showModelInfoDialog(modelName);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
return null; // Return empty array to prevent default menu
|
||||
}
|
||||
|
||||
// Function to show model info dialog using WanLoraInfoDialog
|
||||
function showModelInfoDialog(modelName) {
|
||||
if (!modelName || modelName === "None") {
|
||||
app.ui.dialog.show("No model selected");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[PowerModelLoaderV2] Opening info dialog for model: ${modelName}`);
|
||||
|
||||
// Create a model-specific info dialog by extending WanLoraInfoDialog
|
||||
class WanModelInfoDialog extends WanLoraInfoDialog {
|
||||
async getModelInfo(file) {
|
||||
console.log(`[PowerModelLoaderV2] Fetching info for model: ${file}`);
|
||||
try {
|
||||
const info = await CHECKPOINT_INFO_SERVICE.getInfo(file, false, false);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error(`[PowerModelLoaderV2] Error fetching model info:`, error);
|
||||
// Fallback to basic info
|
||||
return {
|
||||
file: file,
|
||||
name: file,
|
||||
info: {},
|
||||
mtime: Date.now() / 1000
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Override refreshModelInfo to use CHECKPOINT_INFO_SERVICE instead of LORA_INFO_SERVICE
|
||||
async refreshModelInfo(file) {
|
||||
try {
|
||||
const info = await CHECKPOINT_INFO_SERVICE.refreshInfo(file);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error(`[PowerModelLoaderV2] Error refreshing model info from Civitai:`, error);
|
||||
// Return existing info on error instead of failing
|
||||
return this.modelInfo || {
|
||||
file: file,
|
||||
name: file,
|
||||
info: {},
|
||||
mtime: Date.now() / 1000
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and show the dialog
|
||||
const dialog = new WanModelInfoDialog(modelName);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
// Function to show model in Windows Explorer
|
||||
function showModelInExplorer(modelName) {
|
||||
// Use the ComfyUI API to open Explorer for the model file
|
||||
fetch('/wanvideo_wrapper/open_explorer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ lora_name: modelName }) // API expects lora_name but works for models too
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Explorer opened successfully for model:', modelName);
|
||||
} else {
|
||||
console.error('Failed to open Explorer for model:', data.error || 'Unknown error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error opening Explorer for model:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function showModelPicker(event, callback, node = null) {
|
||||
getModels().then(models => {
|
||||
const savedFavorites = loadFavorites();
|
||||
|
||||
// Load UI state from localStorage or node properties
|
||||
let foldersVisible = loadFoldersVisible();
|
||||
let eyeRefreshState = loadEyeRefreshState();
|
||||
let selectedFolder = loadSelectedFolder();
|
||||
|
||||
if (node && node.widgets) {
|
||||
const uiStateWidget = node.widgets.find(w => w.name === "ui_state");
|
||||
if (uiStateWidget && uiStateWidget.value) {
|
||||
try {
|
||||
const uiState = JSON.parse(uiStateWidget.value);
|
||||
if (uiState.foldersVisible !== undefined) foldersVisible = uiState.foldersVisible;
|
||||
if (uiState.eyeRefreshState !== undefined) eyeRefreshState = uiState.eyeRefreshState;
|
||||
if (uiState.selectedFolder !== undefined) selectedFolder = uiState.selectedFolder;
|
||||
} catch (e) {
|
||||
console.warn("[PowerModelLoaderV2] Failed to parse UI state from widget:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use existing LoraPickerDialog for model selection
|
||||
const dialog = new LoraPickerDialog(models, {
|
||||
parentNode: node,
|
||||
callback: callback,
|
||||
sort: "Latest",
|
||||
favorites: savedFavorites,
|
||||
favoritesOnly: false,
|
||||
isModelMode: true, // Flag to indicate this is model mode, not LoRA mode
|
||||
itemType: 'checkpoints', // Explicitly set item type for unified system
|
||||
onFavoriteToggle: (modelName) => {
|
||||
const index = savedFavorites.indexOf(modelName);
|
||||
if (index !== -1) {
|
||||
// Remove from favorites
|
||||
savedFavorites.splice(index, 1);
|
||||
} else {
|
||||
// Add to favorites
|
||||
savedFavorites.push(modelName);
|
||||
}
|
||||
// Save to localStorage using unified system
|
||||
saveFavorites(savedFavorites);
|
||||
// Update dialog's favorites
|
||||
dialog.options.favorites = savedFavorites;
|
||||
// Re-render the list to update star colors
|
||||
dialog.renderList();
|
||||
},
|
||||
foldersVisible: foldersVisible,
|
||||
refreshCallback: async () => {
|
||||
// Perform actual refresh of the model list using unified system
|
||||
try {
|
||||
const freshModels = await getModels(true);
|
||||
dialog.updateLoras(freshModels);
|
||||
await modelDataManager.getPreviewAvailability('checkpoints');
|
||||
|
||||
// Re-render the list to show updated blue dots
|
||||
dialog.renderList();
|
||||
} catch (error) {
|
||||
console.error('[PowerModelLoaderV2] Error refreshing model list:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Override dialog styling to remove height limits
|
||||
const originalShow = dialog.show.bind(dialog);
|
||||
dialog.show = function() {
|
||||
originalShow();
|
||||
// Remove height restrictions from dialog
|
||||
setTimeout(() => {
|
||||
const dialogContent = this.element.querySelector('.litegraph.litecontextmenu.litemenubar-panel');
|
||||
if (dialogContent) {
|
||||
dialogContent.style.maxHeight = 'none';
|
||||
dialogContent.style.height = 'auto';
|
||||
}
|
||||
// Also remove max-height from any scrollable content
|
||||
const scrollableContent = this.element.querySelector('[style*="max-height"]');
|
||||
if (scrollableContent) {
|
||||
scrollableContent.style.maxHeight = 'none';
|
||||
scrollableContent.style.height = 'auto';
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// Initialize dialog's UI state
|
||||
dialog.eyeRefreshState = eyeRefreshState;
|
||||
dialog.selectedFolder = selectedFolder;
|
||||
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "WanVideo.PowerModelLoaderV2",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "CheckpointLoader_v2") {
|
||||
nodeType.prototype.modelsCache = [];
|
||||
|
||||
nodeType.prototype.addCustomWidget = function(widget) {
|
||||
if (!this.widgets) {
|
||||
this.widgets = [];
|
||||
}
|
||||
widget.parent = this;
|
||||
this.widgets.push(widget);
|
||||
return widget;
|
||||
};
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated?.apply(this, arguments);
|
||||
this.serialize_widgets = true;
|
||||
this.modelWidgetsCounter = 0;
|
||||
this.properties = this.properties || {};
|
||||
this.addNonModelWidgets();
|
||||
|
||||
// Always create a single model widget by default
|
||||
this.addNewModelWidget().setModel("None");
|
||||
|
||||
// Ensure minimum size
|
||||
const computed = this.computeSize();
|
||||
this.size = this.size || [0, 0];
|
||||
this.size[0] = Math.max(MINIMUM_NODE_WIDTH, this.size[0], computed[0]);
|
||||
this.size[1] = Math.max(this.size[1], computed[1]);
|
||||
};
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function (info) {
|
||||
onConfigure?.apply(this, arguments);
|
||||
this.modelWidgetsCounter = 0;
|
||||
|
||||
if (this.widgets) {
|
||||
const widgets_to_remove = [...this.widgets];
|
||||
for (const widget of widgets_to_remove) {
|
||||
this.removeWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
this.addNonModelWidgets();
|
||||
|
||||
if (info.widgets_values) {
|
||||
for (const widgetValue of info.widgets_values) {
|
||||
if (widgetValue && widgetValue.model !== undefined) {
|
||||
const widget = this.addNewModelWidget();
|
||||
widget.value = { ...widgetValue };
|
||||
}
|
||||
}
|
||||
}
|
||||
getModels().then((models) => {
|
||||
this.modelsCache = models;
|
||||
});
|
||||
};
|
||||
|
||||
nodeType.prototype.addNewModelWidget = function () {
|
||||
this.modelWidgetsCounter++;
|
||||
const widget = new PowerModelLoaderWidget("model_" + this.modelWidgetsCounter, showModelPicker);
|
||||
widget.callback = (value) => {
|
||||
// Trigger node execution when value changes
|
||||
if (this.onWidgetChanged) {
|
||||
this.onWidgetChanged(widget.name, value, widget.value);
|
||||
}
|
||||
};
|
||||
this.addCustomWidget(widget);
|
||||
if (this.widgetButtonSpacer) {
|
||||
const index = this.widgets.indexOf(this.widgetButtonSpacer);
|
||||
this.widgets.splice(index, 0, this.widgets.pop());
|
||||
}
|
||||
return widget;
|
||||
};
|
||||
|
||||
nodeType.prototype.addNonModelWidgets = function () {
|
||||
this.addCustomWidget(new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
|
||||
|
||||
// Add hidden widget for UI state persistence
|
||||
const uiStateWidget = this.addWidget("string", "ui_state", "{}", () => {},
|
||||
{ serialize: true });
|
||||
uiStateWidget.type = "hidden";
|
||||
};
|
||||
|
||||
// Add width locking mechanism to prevent shrinking below minimum width
|
||||
nodeType.prototype.onResize = function(size) {
|
||||
// Enforce minimum width constraint
|
||||
if (size && size[0] < MINIMUM_NODE_WIDTH) {
|
||||
size[0] = MINIMUM_NODE_WIDTH;
|
||||
}
|
||||
|
||||
// Call the original onResize if it exists
|
||||
if (LGraphNode.prototype.onResize) {
|
||||
return LGraphNode.prototype.onResize.call(this, size);
|
||||
}
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
// Add method to compute size properly
|
||||
nodeType.prototype.computeSize = function(width) {
|
||||
const minWidth = MINIMUM_NODE_WIDTH;
|
||||
const minHeight = 100; // Base height
|
||||
let computedHeight = minHeight;
|
||||
|
||||
// Calculate height based on number of model widgets
|
||||
if (this.widgets) {
|
||||
const modelWidgets = this.widgets.filter(w => w.name?.startsWith("model_"));
|
||||
if (modelWidgets.length > 0) {
|
||||
computedHeight += modelWidgets.length * LiteGraph.NODE_WIDGET_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return [minWidth, computedHeight];
|
||||
};
|
||||
|
||||
// Register slot detection functions for context menu
|
||||
nodeType.prototype.getSlotInPosition = getModelSlotInPosition;
|
||||
nodeType.prototype.getSlotMenuOptions = getModelSlotMenuOptions;
|
||||
|
||||
// Pre-load models
|
||||
getModels().then(models => {
|
||||
nodeType.prototype.modelsCache = models;
|
||||
});
|
||||
|
||||
// Add refreshComboInNode method to handle R key press for reloading definitions
|
||||
nodeType.prototype.refreshComboInNode = function(defs) {
|
||||
// Use unified system to refresh the model list
|
||||
modelDataManager.clearCaches('checkpoints');
|
||||
|
||||
// Fetch fresh models and update cache
|
||||
getModels(true).then((modelsDetails) => {
|
||||
nodeType.prototype.modelsCache = modelsDetails;
|
||||
|
||||
// Update all nodes of this type
|
||||
app.graph._nodes.forEach(node => {
|
||||
if (node.type === "CheckpointLoader_v2") {
|
||||
// Update any existing widgets that might need the fresh data
|
||||
for (const widget of node.widgets || []) {
|
||||
if (widget.name?.startsWith("model_")) {
|
||||
// Update the widget's parent reference to this node
|
||||
widget.parent = node;
|
||||
|
||||
// Trigger a value change to update the UI
|
||||
if (widget.callback && typeof widget.callback === 'function') {
|
||||
widget.callback(widget.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a redraw
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
});
|
||||
|
||||
}).catch(error => {
|
||||
console.error('[PowerModelLoaderV2] Error refreshing model list:', error);
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Unified Data Management Module for LoRA and Model Previews
|
||||
* Eliminates code duplication across power_lora_loader.js, power_lora_loader_v2.js, and power_model_loader_v2.js
|
||||
*/
|
||||
|
||||
import { rgthreeApi as wanvidApi } from "./rgthree_api.js";
|
||||
|
||||
// ============================================================================
|
||||
// DIRECTORY STRUCTURE MANAGER
|
||||
// ============================================================================
|
||||
|
||||
class DirectoryStructureManager {
|
||||
/**
|
||||
* Get the base directory for the given item type
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {string} The base directory name
|
||||
*/
|
||||
static getBaseDirectory(type) {
|
||||
return type === 'checkpoints' ? 'checkpoints' : 'loras';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preview directory for the given item type
|
||||
* @returns {string} The preview directory path
|
||||
*/
|
||||
static getPreviewDirectory() {
|
||||
// Both types use _power_preview in their respective directories
|
||||
return '_power_preview';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API parameters for the given type
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {Object} API parameters
|
||||
*/
|
||||
static getApiParams(type) {
|
||||
return {
|
||||
isModel: type === 'checkpoints',
|
||||
is_model: type === 'checkpoints'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UI STATE MANAGER
|
||||
// ============================================================================
|
||||
|
||||
class UIManager {
|
||||
/**
|
||||
* Get localStorage key for the given type and suffix
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {string} suffix - 'Cache', 'favorites', 'foldersVisible', etc.
|
||||
* @returns {string} localStorage key
|
||||
*/
|
||||
static getStorageKey(type, suffix) {
|
||||
const prefix = type === 'checkpoints' ? 'powerModelLoaderV2' : 'wanVideoPowerLoraLoader';
|
||||
return `${prefix}.${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save favorites to localStorage
|
||||
* @param {Array} favorites - Array of favorite item names
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
*/
|
||||
static saveFavorites(favorites, type) {
|
||||
const key = this.getStorageKey(type, 'favorites');
|
||||
localStorage.setItem(key, JSON.stringify(favorites));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load favorites from localStorage
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {Array} Array of favorite item names
|
||||
*/
|
||||
static loadFavorites(type) {
|
||||
const key = this.getStorageKey(type, 'favorites');
|
||||
const favorites = localStorage.getItem(key);
|
||||
return favorites !== null ? JSON.parse(favorites) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save UI state to localStorage
|
||||
* @param {Object} state - UI state object
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
*/
|
||||
static saveUIState(state, type) {
|
||||
const key = this.getStorageKey(type, 'uiState');
|
||||
localStorage.setItem(key, JSON.stringify(state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load UI state from localStorage
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {Object} UI state object
|
||||
*/
|
||||
static loadUIState(type) {
|
||||
const key = this.getStorageKey(type, 'uiState');
|
||||
const state = localStorage.getItem(key);
|
||||
return state !== null ? JSON.parse(state) : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save individual UI state properties
|
||||
* @param {string} property - Property name
|
||||
* @param {any} value - Property value
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
*/
|
||||
static saveUIProperty(property, value, type) {
|
||||
const key = this.getStorageKey(type, property);
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load individual UI state properties
|
||||
* @param {string} property - Property name
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {any} defaultValue - Default value if not found
|
||||
* @returns {any} Property value or default
|
||||
*/
|
||||
static loadUIProperty(property, type, defaultValue = null) {
|
||||
const key = this.getStorageKey(type, property);
|
||||
const value = localStorage.getItem(key);
|
||||
return value !== null ? JSON.parse(value) : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MODEL DATA MANAGER
|
||||
// ============================================================================
|
||||
|
||||
class ModelDataManager {
|
||||
constructor() {
|
||||
this.caches = {
|
||||
loras: null,
|
||||
checkpoints: null
|
||||
};
|
||||
this.lastLoadTimes = {
|
||||
loras: 0,
|
||||
checkpoints: 0
|
||||
};
|
||||
this.previewLoadPromises = {
|
||||
loras: null,
|
||||
checkpoints: null
|
||||
};
|
||||
this.previewAvailabilityCaches = {
|
||||
loras: new Set(),
|
||||
checkpoints: new Set()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items (LoRAs or Models) with caching
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {boolean} forceRefresh - Force refresh from API
|
||||
* @returns {Promise<Array>} Array of items with name and mtime
|
||||
*/
|
||||
async getItems(type = 'loras', forceRefresh = false) {
|
||||
// Check if we have cached data and it's not expired
|
||||
const now = Date.now();
|
||||
const cacheKey = type;
|
||||
const storageKey = UIManager.getStorageKey(type, 'Cache');
|
||||
|
||||
if (!forceRefresh && this.caches[cacheKey]) {
|
||||
const cachedData = localStorage.getItem(storageKey);
|
||||
if (cachedData) {
|
||||
try {
|
||||
const parsed = JSON.parse(cachedData);
|
||||
if (parsed && parsed.length > 0) {
|
||||
return this.caches[cacheKey];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[${type.toUpperCase()}] Failed to parse cached data:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cache if force refresh is requested
|
||||
if (forceRefresh) {
|
||||
this.caches[cacheKey] = null;
|
||||
localStorage.removeItem(storageKey);
|
||||
}
|
||||
|
||||
try {
|
||||
let itemsData;
|
||||
|
||||
// Use appropriate rgthreeApi method
|
||||
if (type === 'checkpoints') {
|
||||
itemsData = await wanvidApi.getCheckpoints(forceRefresh);
|
||||
} else {
|
||||
itemsData = await wanvidApi.getLoras(forceRefresh);
|
||||
}
|
||||
|
||||
// Convert rgthreeApi format to our expected format
|
||||
this.caches[cacheKey] = itemsData.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return { name: item, mtime: 0 };
|
||||
}
|
||||
return {
|
||||
name: item.name || item.filename || item.file || JSON.stringify(item),
|
||||
mtime: item.mtime || item.modified || item.modified_time || 0
|
||||
};
|
||||
});
|
||||
|
||||
// Cache the data in localStorage
|
||||
localStorage.setItem(storageKey, JSON.stringify(this.caches[cacheKey]));
|
||||
this.lastLoadTimes[cacheKey] = now;
|
||||
|
||||
return this.caches[cacheKey];
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${type.toUpperCase()}] Failed to fetch items:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview availability with caching and debouncing
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {Promise<Set>} Set of item names that have previews
|
||||
*/
|
||||
async getPreviewAvailability(type = 'loras') {
|
||||
// Prevent multiple simultaneous calls only
|
||||
const cacheKey = type;
|
||||
if (this.previewLoadPromises[cacheKey]) {
|
||||
console.log(`[Preview] Preview load already in progress for ${type}, waiting...`);
|
||||
return this.previewLoadPromises[cacheKey];
|
||||
}
|
||||
|
||||
this.previewLoadPromises[cacheKey] = this._loadPreviewAvailabilityInternal(type);
|
||||
|
||||
try {
|
||||
const result = await this.previewLoadPromises[cacheKey];
|
||||
return result;
|
||||
} finally {
|
||||
this.previewLoadPromises[cacheKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to load preview availability
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {Promise<Set>} Set of item names that have previews
|
||||
*/
|
||||
async _loadPreviewAvailabilityInternal(type) {
|
||||
try {
|
||||
const apiParams = DirectoryStructureManager.getApiParams(type);
|
||||
const url = `/wanvid/api/loras/previews${apiParams.isModel ? '?is_model=true' : ''}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.status === 200 && data.previews) {
|
||||
this.previewAvailabilityCaches[type].clear();
|
||||
data.previews.forEach(preview => {
|
||||
this.previewAvailabilityCaches[type].add(preview.lora);
|
||||
});
|
||||
return this.previewAvailabilityCaches[type];
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Preview] API returned non-OK status for ${type}:`, response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Preview] Error loading preview availability for ${type}:`, error);
|
||||
// Don't clear cache on error, keep existing data
|
||||
}
|
||||
|
||||
return this.previewAvailabilityCaches[type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item has preview available
|
||||
* @param {string} itemName - Name of the item
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @returns {boolean} True if preview is available
|
||||
*/
|
||||
hasPreview(itemName, type = 'loras') {
|
||||
return this.previewAvailabilityCaches[type].has(itemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a preview image for an item
|
||||
* @param {File} imageFile - The image file to save
|
||||
* @param {string} itemName - Name of the item
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {string} suffix - Optional suffix for the image
|
||||
* @param {string} subfolder - Optional subfolder path
|
||||
* @returns {Promise<Object>} Response from the API
|
||||
*/
|
||||
async savePreviewImage(imageFile, itemName, type, suffix = '', subfolder = '') {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
if (type === 'checkpoints') {
|
||||
formData.append('model_name', itemName);
|
||||
formData.append('model_path', subfolder);
|
||||
} else {
|
||||
formData.append('lora_name', itemName);
|
||||
formData.append('lora_path', subfolder);
|
||||
}
|
||||
formData.append('suffix', suffix);
|
||||
formData.append('is_model', type === 'checkpoints' ? 'true' : 'false');
|
||||
|
||||
const response = await fetch('/wanvid/api/lora/preview-image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save preview image: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save JSON data for an item
|
||||
* @param {Object} jsonData - JSON data to save
|
||||
* @param {string} itemName - Name of the item
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {string} subfolder - Optional subfolder path
|
||||
* @returns {Promise<Object>} Response from the API
|
||||
*/
|
||||
async saveJsonData(jsonData, itemName, type, subfolder = '') {
|
||||
const formData = new FormData();
|
||||
formData.append('json', JSON.stringify(jsonData));
|
||||
formData.append(type === 'checkpoints' ? 'model_name' : 'lora_name', itemName);
|
||||
formData.append(type === 'checkpoints' ? 'model_path' : 'lora_path', subfolder);
|
||||
|
||||
const endpoint = type === 'checkpoints' ?
|
||||
'/wanvid/api/model/preview-json' :
|
||||
'/wanvid/api/lora/preview-json';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save JSON data: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview image URL
|
||||
* @param {string} itemName - Name of the item (without extension)
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {string} suffix - Optional suffix
|
||||
* @param {string} subfolder - Optional subfolder
|
||||
* @returns {string} Preview image URL
|
||||
*/
|
||||
getPreviewImageUrl(itemName, type, suffix = '', subfolder = '') {
|
||||
const apiParams = DirectoryStructureManager.getApiParams(type);
|
||||
const url = new URL('/wanvid/api/loras/preview', window.location.origin);
|
||||
url.searchParams.set('file', itemName);
|
||||
|
||||
if (subfolder) {
|
||||
url.searchParams.set('subfolder', subfolder);
|
||||
}
|
||||
|
||||
if (suffix) {
|
||||
url.searchParams.set('suffix', suffix);
|
||||
}
|
||||
|
||||
if (apiParams.isModel) {
|
||||
url.searchParams.set('is_model', 'true');
|
||||
}
|
||||
|
||||
const finalUrl = url.toString();
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON data for an item
|
||||
* @param {string} itemName - Name of the item (without extension)
|
||||
* @param {string} type - 'loras' or 'checkpoints'
|
||||
* @param {string} subfolder - Optional subfolder
|
||||
* @returns {Promise<Object|null>} JSON data or null if not found
|
||||
*/
|
||||
async getJsonData(itemName, type, subfolder = '') {
|
||||
const apiParams = DirectoryStructureManager.getApiParams(type);
|
||||
const jsonFilename = itemName + '.json';
|
||||
|
||||
const attempts = [
|
||||
// Method 1: Try the modified preview endpoint that now supports JSON
|
||||
`/wanvid/api/loras/preview?file=${jsonFilename}${subfolder ? '&subfolder=' + subfolder : ''}${apiParams.isModel ? '&is_model=true' : ''}`,
|
||||
];
|
||||
|
||||
// Add type-specific fallbacks
|
||||
if (apiParams.isModel) {
|
||||
attempts.push(
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`,
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview/${subfolder}&type=models`,
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`
|
||||
);
|
||||
} else {
|
||||
attempts.push(
|
||||
`/api/view?filename=${jsonFilename}&subfolder=${subfolder}/_power_preview&type=models`,
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview/${subfolder}&type=models`,
|
||||
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`
|
||||
);
|
||||
}
|
||||
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
const response = await fetch(attempt);
|
||||
if (response.ok) {
|
||||
const jsonData = await response.json();
|
||||
return jsonData;
|
||||
}
|
||||
} catch (attemptError) {
|
||||
console.log(`[PullInfo Debug] JSON attempt failed: ${attemptError}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
* @param {string} type - 'loras', 'checkpoints', or 'all'
|
||||
*/
|
||||
clearCaches(type = 'all') {
|
||||
if (type === 'all' || type === 'loras') {
|
||||
this.caches.loras = null;
|
||||
this.previewAvailabilityCaches.loras.clear();
|
||||
localStorage.removeItem(UIManager.getStorageKey('loras', 'Cache'));
|
||||
}
|
||||
|
||||
if (type === 'all' || type === 'checkpoints') {
|
||||
this.caches.checkpoints = null;
|
||||
this.previewAvailabilityCaches.checkpoints.clear();
|
||||
localStorage.removeItem(UIManager.getStorageKey('checkpoints', 'Cache'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
// Create singleton instance
|
||||
const modelDataManager = new ModelDataManager();
|
||||
|
||||
export {
|
||||
ModelDataManager,
|
||||
UIManager,
|
||||
DirectoryStructureManager,
|
||||
modelDataManager
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
// Shared utility functions for LoRA renaming
|
||||
|
||||
/**
|
||||
* Shows a temporary non-blocking message
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} type - 'success', 'warning', or 'error'
|
||||
*/
|
||||
function showTemporaryMessage(message, type = 'success') {
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.style.cssText = `
|
||||
position: fixed !important;
|
||||
top: 20px !important;
|
||||
right: 20px !important;
|
||||
background-color: ${
|
||||
type === 'success' ? '#2a5a2a' :
|
||||
type === 'warning' ? '#5a5a2a' :
|
||||
'#5a2a2a'
|
||||
} !important;
|
||||
color: #ddd !important;
|
||||
padding: 12px 16px !important;
|
||||
border-radius: 6px !important;
|
||||
border: 1px solid ${
|
||||
type === 'success' ? '#4a7a4a' :
|
||||
type === 'warning' ? '#7a7a4a' :
|
||||
'#7a4a4a'
|
||||
} !important;
|
||||
font-family: Arial, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
z-index: 10002 !important;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.3s ease-in-out !important;
|
||||
max-width: 350px !important;
|
||||
word-wrap: break-word !important;
|
||||
`;
|
||||
|
||||
messageElement.textContent = message;
|
||||
document.body.appendChild(messageElement);
|
||||
|
||||
// Fade in
|
||||
setTimeout(() => {
|
||||
messageElement.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// Remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
messageElement.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (messageElement.parentNode) {
|
||||
messageElement.parentNode.removeChild(messageElement);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a LoRA file and its associated files
|
||||
* @param {string} loraName - The current LoRA file path
|
||||
* @param {string} variant - 'high' for main LoRA, 'low' for low variant
|
||||
* @param {Function} [refreshCallback] - Optional callback to refresh the UI after renaming
|
||||
* @param {Function} [widgetCallback] - Optional widget callback for UI updates
|
||||
* @param {Object} [node] - Optional node for canvas updates
|
||||
* @param {Object} [widget] - Optional widget for value updates
|
||||
*/
|
||||
export async function renameLoRAFiles(loraName, variant = 'high', refreshCallback = null, widgetCallback = null, node = null, widget = null) {
|
||||
if (!loraName) {
|
||||
console.error('No LoRA name found for renaming');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract filename without extension
|
||||
const fileName = loraName.includes('/') || loraName.includes('\\')
|
||||
? loraName.split(/[\/\\]/).pop()
|
||||
: loraName;
|
||||
|
||||
const baseName = fileName.replace(/\.safetensors$/, '');
|
||||
|
||||
// Show a simple prompt for the new name
|
||||
const newName = prompt(`Enter new name for LoRA:`, baseName);
|
||||
|
||||
if (!newName || newName.trim() === '') {
|
||||
return; // User cancelled or entered empty name
|
||||
}
|
||||
|
||||
const finalNewName = newName.trim().replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize name
|
||||
|
||||
if (finalNewName === baseName) {
|
||||
return; // No change
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/wanvideo_wrapper/rename_lora', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
old_name: loraName,
|
||||
new_name: finalNewName,
|
||||
variant: variant
|
||||
})
|
||||
});
|
||||
|
||||
// Check if response is ok before trying to parse JSON
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Server returned error:', response.status, errorText);
|
||||
throw new Error(`Server error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
// Try to parse JSON with better error handling
|
||||
let data;
|
||||
let responseText;
|
||||
try {
|
||||
responseText = await response.text();
|
||||
console.log('Raw response:', responseText); // Debug log
|
||||
data = JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error('JSON parse error:', parseError);
|
||||
console.error('Raw response text:', responseText);
|
||||
throw new Error(`Invalid JSON response from server: ${responseText ? responseText.substring(0, 200) : 'Empty response'}`);
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
// Handle widget updates if widget and node are provided
|
||||
if (widget && node) {
|
||||
// Update the widget value(s) based on which variant was renamed
|
||||
if (variant === 'low') {
|
||||
widget.value.low_variant_name = data.new_low_path;
|
||||
if (widget.value.is_low && widget.value.low_variant_name) {
|
||||
widget.value.lora_name = data.new_low_path;
|
||||
}
|
||||
} else {
|
||||
widget.value.lora_name = data.new_high_path;
|
||||
// Also update low variant if it exists and is based on the high one
|
||||
if (widget.value.is_low && widget.value.low_variant_name &&
|
||||
widget.value.low_variant_name.includes(baseName)) {
|
||||
widget.value.low_variant_name = data.new_low_path || data.new_high_path;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the displayed name
|
||||
if (widgetCallback) {
|
||||
widgetCallback(widget.value, null, null, null);
|
||||
}
|
||||
|
||||
// Mark canvas as dirty to trigger redraw
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// Handle refresh callback (for picker dialog)
|
||||
if (refreshCallback) {
|
||||
// Show success message in console and call refresh
|
||||
console.log('LoRA renamed successfully:', data.message);
|
||||
|
||||
// Show a temporary success message instead of blocking alert
|
||||
showTemporaryMessage(`✅ LoRA renamed to: ${finalNewName}.safetensors`, 'success');
|
||||
|
||||
try {
|
||||
await refreshCallback();
|
||||
console.log('Refresh callback executed successfully');
|
||||
} catch (refreshError) {
|
||||
console.warn('Refresh callback failed:', refreshError);
|
||||
showTemporaryMessage('⚠️ List may need manual refresh to see updated name', 'warning');
|
||||
}
|
||||
} else if (!widget) {
|
||||
// Show success message for non-widget, non-picker contexts
|
||||
console.log('LoRA renamed successfully:', data.message);
|
||||
showTemporaryMessage(`✅ LoRA renamed to: ${finalNewName}.safetensors`, 'success');
|
||||
|
||||
// If we have access to the dialog, refresh preview availability for blue dots
|
||||
const dialog = document.querySelector('.lora-picker-dialog');
|
||||
if (dialog && dialog.dialogInstance && dialog.dialogInstance.loadPreviewAvailability) {
|
||||
try {
|
||||
console.log('Refreshing preview availability after rename...');
|
||||
await dialog.dialogInstance.loadPreviewAvailability();
|
||||
// Re-render list to update blue dots
|
||||
if (dialog.dialogInstance.renderList) {
|
||||
dialog.dialogInstance.renderList();
|
||||
}
|
||||
console.log('Preview availability refreshed after rename');
|
||||
} catch (previewError) {
|
||||
console.warn('Failed to refresh preview availability after rename:', previewError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert('Failed to rename LoRA: ' + (data.error || 'Unknown error'));
|
||||
console.error('Failed to rename LoRA:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error renaming LoRA: ' + error.message);
|
||||
console.error('Error renaming LoRA:', error);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,14 @@ class RgthreeApi {
|
||||
}
|
||||
return this.getLorasPromise;
|
||||
}
|
||||
getCheckpoints(force = false) {
|
||||
if (!this.getCheckpointsPromise || force) {
|
||||
// Add a timestamp to prevent caching when force is true
|
||||
const url = force ? `/checkpoints?format=details&t=${Date.now()}` : "/checkpoints?format=details";
|
||||
this.getCheckpointsPromise = this.fetchJson(url, { cache: "no-store" });
|
||||
}
|
||||
return this.getCheckpointsPromise;
|
||||
}
|
||||
async fetchApiJsonOrNull(route, options) {
|
||||
const response = await this.fetchJson(route, options);
|
||||
if (response.status === 200 && response.data) {
|
||||
|
||||
@@ -596,7 +596,7 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
|
||||
constructor(name, showLoraChooser) {
|
||||
super(name);
|
||||
this.showLoraChooser = showLoraChooser;
|
||||
this.value = { on: true, lora: "None", strength: 1, low_strength: 1, is_low: false };
|
||||
this.value = { on: true, lora: "None", strength: 1, low_strength: 1, is_low: false, low_active: true };
|
||||
this.haveMouseMovedStrength = false;
|
||||
this.hitAreas = {
|
||||
toggle: { bounds: [0, 0], onDown: this.onToggleDown },
|
||||
@@ -610,12 +610,17 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
|
||||
set value(v) {
|
||||
this._value = v;
|
||||
if (typeof this._value !== "object") {
|
||||
this._value = { on: true, lora: "None", strength: 1, low_strength: 1, is_low: false };
|
||||
this._value = { on: true, lora: "None", strength: 1, low_strength: 1, is_low: false, low_active: true };
|
||||
}
|
||||
// Ensure low_strength is always initialized
|
||||
if (this._value.low_strength === undefined) {
|
||||
this._value.low_strength = 1;
|
||||
}
|
||||
|
||||
// Ensure low_active is always initialized (default to true for backward compatibility)
|
||||
if (this._value.low_active === undefined) {
|
||||
this._value.low_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
@@ -634,7 +639,11 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
|
||||
posX += this.hitAreas.toggle.bounds[1] + MARGIN;
|
||||
|
||||
let rposX = w - MARGIN;
|
||||
ctx.fillStyle = this.value.is_low ? "lime" : "#555";
|
||||
if (this.value.is_low) {
|
||||
ctx.fillStyle = this.value.low_active ? "lime" : "orange";
|
||||
} else {
|
||||
ctx.fillStyle = "#555";
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(rposX - ICON_WIDTH / 2, y + h / 2, h * 0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
@@ -759,6 +768,9 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
|
||||
// Import and show the LoRA info dialog
|
||||
import("./dialog_info.js").then(({ WanLoraInfoDialog }) => {
|
||||
const infoDialog = new WanLoraInfoDialog(loraToShow).show();
|
||||
// Store the item type for use in preview generation (this is for LoRA widgets)
|
||||
infoDialog.itemType = 'loras';
|
||||
console.log(`[Widgets Debug] Created LoRA widget dialog with itemType: ${infoDialog.itemType}`);
|
||||
infoDialog.addEventListener("close", ((e) => {
|
||||
if (e.detail.dirty) {
|
||||
// Dialog was modified, could trigger refresh if needed
|
||||
@@ -777,16 +789,19 @@ File: ${loraToShow}`;
|
||||
|
||||
serializeValue(node, index) {
|
||||
const v = { ...this.value };
|
||||
|
||||
|
||||
// Ensure low_strength is always included
|
||||
if (v.low_strength === undefined) {
|
||||
v.low_strength = 1;
|
||||
}
|
||||
|
||||
|
||||
// Ensure is_low and low_variant_name are preserved
|
||||
v.is_low = this.value.is_low || false;
|
||||
v.low_variant_name = this.value.low_variant_name || null;
|
||||
|
||||
|
||||
// Ensure low_active is preserved (default to true for backward compatibility)
|
||||
v.low_active = this.value.low_active !== undefined ? this.value.low_active : true;
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user