cleanup changes

This commit is contained in:
JJ
2026-11-08 15:40:13 +00:00
parent 967cf351e1
commit 2e93ef2267
46 changed files with 859 additions and 901 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ from .nodes.masks import *
# Import API endpoints to register routes
from . import api
from .utility.rgthree_api import routes_model_info
from .utility.wanvid_api import routes_model_info
from .nodes import trigger_ref_refresh # Register PrepareRefs backend trigger endpoint
NODE_CONFIG = {}
+15 -2
View File
@@ -447,11 +447,24 @@ async def rename_lora(request) -> web.Response:
if os.path.exists(preview_folder):
renamed_files.extend(_rename_preview_files(old_base_name, new_name, preview_folder))
# Compute the ComfyUI-relative name (e.g. "subdir/name.safetensors") for
# UI use - widgets store partial paths, not full filesystem paths.
new_relative_path = new_lora_path
for folder in folders_to_check:
try:
rel = os.path.relpath(new_lora_path, folder)
if not rel.startswith('..'):
new_relative_path = rel.replace(os.sep, '/')
break
except ValueError:
# Different drive letters on Windows, skip this folder
continue
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
"new_high_path": new_relative_path,
"new_low_path": new_relative_path, # For high variant, low path is the same
"renamed_files": renamed_files
})
+2 -2
View File
@@ -27,7 +27,7 @@ class LoraReduceRank:
EXPERIMENTAL = True
DESCRIPTION = "Resize a LoRA model by reducing it's rank. Based on kohya's sd-scripts."
CATEGORY = "KJNodes/lora"
CATEGORY = "QQ/lora"
def save(self, lora_name, new_rank, output_dtype, dynamic_method, dynamic_param, verbose):
@@ -110,7 +110,7 @@ class LoraRemapStrength:
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "KJNodes/lora"
CATEGORY = "QQ/lora"
def save(self, lora_name, reference_strength, target_strength, create_missing_alpha, output_dtype):
print(f"[LoraRemapStrength DEBUG] Starting with inputs:")
+1 -1
View File
@@ -271,7 +271,7 @@ class LoraSmartCombine:
RETURN_TYPES = ()
FUNCTION = "combine"
OUTPUT_NODE = True
CATEGORY = "KJNodes/lora"
CATEGORY = "QQ/lora"
DESCRIPTION = "Smartly combine multiple LoRAs with rank extension using SVD decomposition"
def combine(self, lora_1_name, lora_2_name, lora_3_name, lora_4_name,
+1 -1
View File
@@ -379,7 +379,7 @@ class LoraExtractKJv2:
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "KJNodes/lora"
CATEGORY = "QQ/lora"
def save(self, finetuned_model, original_model, filename_prefix, rank, lora_type, algorithm,
lowrank_iters, output_dtype, bias_diff, adaptive_param, clamp_quantile,
+4 -4
View File
@@ -46,7 +46,7 @@ class LoadModelMeta:
RETURN_TYPES = ("MODEL",)
FUNCTION = "load_checkpoint"
CATEGORY = "KJNodes/ltxv"
CATEGORY = "QQ/ltxv"
def load_checkpoint(self, checkpoint_name):
"""
@@ -156,7 +156,7 @@ class LTX2LoRASelect:
RETURN_TYPES = ("MODEL",)
FUNCTION = "apply_lora"
CATEGORY = "KJNodes/ltxv"
CATEGORY = "QQ/ltxv"
def apply_lora(
self,
@@ -357,7 +357,7 @@ class LoraMergeLTX:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("merged_lora_path",)
FUNCTION = "merge_loras"
CATEGORY = "KJNodes/ltxv"
CATEGORY = "QQ/ltxv"
OUTPUT_NODE = True
def merge_loras(self, model_with_loras, output_prefix, rank, output_dir, merge_on_model):
@@ -622,7 +622,7 @@ class LoraApplyOnLTX:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("saved_checkpoint_path",)
FUNCTION = "apply_lora_to_model"
CATEGORY = "KJNodes/ltxv"
CATEGORY = "QQ/ltxv"
OUTPUT_NODE = True
def apply_lora_to_model(self, model_with_loras, output_prefix, output_dir):
+1 -1
View File
@@ -90,7 +90,7 @@ class PowerCompareVideo:
"""
Playback preview node: receives an IMAGE batch (video frames) and sends
them to the client UI for timeline-based playback with a sliding A/B
comparison (rgthree Image Comparer style).
comparison (Image Comparer style).
Inputs:
images - IMAGE tensor [frame_count, height, width, 3] (e.g. from PowerLoadVideo).
+8 -38
View File
@@ -59,7 +59,7 @@ any_type = AnyType("*")
def get_lora_by_filename_basic(file_path, lora_paths=None, log_node=None):
"""Basic implementation of LoRA path resolution based on rgthree-comfy's get_lora_by_filename."""
"""Basic LoRA path resolution helper."""
if lora_paths is None:
lora_paths = folder_paths.get_filename_list("loras")
@@ -124,7 +124,7 @@ def get_lora_by_filename_basic(file_path, lora_paths=None, log_node=None):
class WanVideoPowerLoraLoader:
"""A power lora loader for WanVideo that supports unlimited loras with rgthree-style UI."""
"""A power lora loader for WanVideo that supports unlimited loras with a custom-styled UI."""
def __init__(self):
self.properties = {
@@ -150,7 +150,7 @@ class WanVideoPowerLoraLoader:
RETURN_NAMES = ("selected_lora", "low_lora")
FUNCTION = "load_loras"
CATEGORY = "WanVideoWrapper_QQ/utils"
DESCRIPTION = "A powerful, flexible node to load multiple WanVideo loras with rgthree-style UI"
DESCRIPTION = "A powerful, flexible node to load multiple WanVideo loras with a custom-styled UI"
def load_loras(self, prev_lora=None, blocks=None, **kwargs):
"""Loops over the provided loras in kwargs and applies valid ones."""
@@ -188,13 +188,7 @@ class WanVideoPowerLoraLoader:
if not lora_name or lora_name.lower() == "none":
continue
# Use the sophisticated LoRA path resolution from rgthree-comfy
try:
from rgthree_comfy.py.power_prompt_utils import get_lora_by_filename
lora_filename = get_lora_by_filename(lora_name, log_node=self.__class__.__name__)
except ImportError:
# Fallback to basic path resolution if rgthree-comfy is not available
lora_filename = get_lora_by_filename_basic(lora_name, log_node=self.__class__.__name__)
lora_filename = get_lora_by_filename_basic(lora_name, log_node=self.__class__.__name__)
if lora_filename is None:
print(f"[WanVideoPowerLoraLoader] LoRA '{lora_name}' not found, skipping.")
@@ -314,13 +308,7 @@ class WanVideoPowerLoraLoader:
if low_strength is None:
low_strength = 0.0
# Use the sophisticated LoRA path resolution for low variant
try:
from rgthree_comfy.py.power_prompt_utils import get_lora_by_filename
low_variant_filename = get_lora_by_filename(low_variant_name, log_node=self.__class__.__name__)
except ImportError:
# Fallback to basic path resolution if rgthree-comfy is not available
low_variant_filename = get_lora_by_filename_basic(low_variant_name, log_node=self.__class__.__name__)
low_variant_filename = get_lora_by_filename_basic(low_variant_name, log_node=self.__class__.__name__)
if low_variant_filename is None:
print(f"[WanVideoPowerLoraLoader] Low variant LoRA '{low_variant_name}' not found, skipping low variant.")
@@ -426,13 +414,7 @@ class PowerLoraLoaderV2:
if not lora_name or lora_name.lower() == "none":
continue
# Use the sophisticated LoRA path resolution from rgthree-comfy
try:
from rgthree_comfy.py.power_prompt_utils import get_lora_by_filename
lora_filename = get_lora_by_filename(lora_name, log_node=self.__class__.__name__)
except ImportError:
# Fallback to basic path resolution if rgthree-comfy is not available
lora_filename = get_lora_by_filename_basic(lora_name, log_node=self.__class__.__name__)
lora_filename = get_lora_by_filename_basic(lora_name, log_node=self.__class__.__name__)
if lora_filename is None:
# Try to get the full path directly from ComfyUI as a fallback
@@ -573,13 +555,7 @@ class PowerLoraLoaderV2:
if low_strength is None:
low_strength = 0.0
# Use the sophisticated LoRA path resolution for low variant
try:
from rgthree_comfy.py.power_prompt_utils import get_lora_by_filename
low_variant_filename = get_lora_by_filename(low_variant_name, log_node=self.__class__.__name__)
except ImportError:
# Fallback to basic path resolution if rgthree-comfy is not available
low_variant_filename = get_lora_by_filename_basic(low_variant_name, log_node=self.__class__.__name__)
low_variant_filename = get_lora_by_filename_basic(low_variant_name, log_node=self.__class__.__name__)
if low_variant_filename is None:
print(f"[PowerLoraLoaderV2] Low variant LoRA '{low_variant_name}' not found, skipping low variant.")
@@ -689,13 +665,7 @@ class SimplePowerLoraLoader:
if not lora_name or lora_name.lower() == "none":
continue
# Use the sophisticated LoRA path resolution from rgthree-comfy
try:
from rgthree_comfy.py.power_prompt_utils import get_lora_by_filename
lora_filename = get_lora_by_filename(lora_name, log_node=self.__class__.__name__)
except ImportError:
# Fallback to basic path resolution if rgthree-comfy is not available
lora_filename = get_lora_by_filename_basic(lora_name, log_node=self.__class__.__name__)
lora_filename = get_lora_by_filename_basic(lora_name, log_node=self.__class__.__name__)
if lora_filename is None:
# Try to get the full path directly from ComfyUI as a fallback
@@ -128,7 +128,7 @@ async def get_model_info(
should_save = False
# basic data
basic_data = get_file_info(file, model_type)
# Try to load a rgthree-info.json file next to the file.
# Try to load the model info json file next to the file.
info_data = get_model_info_file_data(file, model_type, default={})
for key in ['file', 'path', 'modified', 'imageLocal', 'hasInfoFile']:
@@ -196,7 +196,7 @@ async def get_model_info(
save_model_info(file, info_data, model_type)
# If we're saving, then the UI is likely waiting to see if the refreshed data is coming in.
await PromptServer.instance.send(f"rgthree-refreshed-{model_type}-info", {"data": info_data})
await PromptServer.instance.send(f"wanvid-refreshed-{model_type}-info", {"data": info_data})
return info_data
@@ -25,12 +25,12 @@ def is_param_truthy(request, param):
def set_default_page_resources(path, routes):
""" Sets up routes for handling static files under a path."""
@routes.get(f'/rgthree/{path}/{{file}}')
@routes.get(f'/wanvid/{path}/{{file}}')
async def get_resource(request):
""" Returns a resource file. """
return web.FileResponse(os.path.join(DIR_WEB, path, request.match_info['file']))
@routes.get(f'/rgthree/{path}/{{subdir}}/{{file}}')
@routes.get(f'/wanvid/{path}/{{subdir}}/{{file}}')
async def get_resource_subdir(request):
""" Returns a resource file. """
return web.FileResponse(
@@ -38,14 +38,14 @@ def set_default_page_resources(path, routes):
def set_default_page_routes(path, routes):
""" Sets default path handling for a hosted rgthree page. """
""" Sets default path handling for a hosted page. """
@routes.get(f'/rgthree/{path}')
@routes.get(f'/wanvid/{path}')
async def get_path_redir(request):
""" Redirects to the path adding a trailing slash. """
raise web.HTTPFound(f'{request.path}/')
@routes.get(f'/rgthree/{path}/')
@routes.get(f'/wanvid/{path}/')
async def get_path_index(request):
""" Handles the page's index loading. """
html = ''
+1 -2
View File
@@ -351,8 +351,7 @@ function getComfyDefaultLinkColor(canvas) {
function linkHighlighted(canvas, targetNode, sourceNode) {
return Boolean(
targetNode?.selected || sourceNode?.selected ||
canvas?.selectedItems?.has?.(targetNode) || canvas?.selectedItems?.has?.(sourceNode) ||
canvas?.selected_nodes?.[targetNode?.id] || canvas?.selected_nodes?.[sourceNode?.id]
canvas?.selectedItems?.has?.(targetNode) || canvas?.selectedItems?.has?.(sourceNode)
);
}
@@ -19,9 +19,9 @@
* the top-left corner of its video's region in the stitched output.
*/
import { app } from '../../../scripts/app.js';
import { RgthreeBaseWidget } from '../power_spline_editor/drawing_utils.js';
import { QQBaseWidget } from '../power_spline_editor/drawing_utils.js';
export class PowerCompareRowWidget extends RgthreeBaseWidget {
export class PowerCompareRowWidget extends QQBaseWidget {
constructor(name = "PowerCompareRow") {
super(name);
this.type = "custom";
@@ -7,7 +7,7 @@
*
* When a second video is available (previous run's cache, or the images_b
* input), the row above the playback area offers three compare modes:
* - slide (default): rgthree-Image-Comparer-style slider on the playback
* - slide (default): Image-Comparer-style slider on the playback
* area - the divider follows the mouse, revealing B on the left and
* keeping A on the right
* - right: B placed on the right side of A
@@ -290,7 +290,7 @@ app.registerExtension({
};
// Left-to-right sliding compare: the divider follows the mouse while
// it moves over the playback area (borrowed from rgthree comparer).
// it moves over the playback area (borrowed from qq comparer).
// Only active in slide mode with a second video present.
const updateSplitFromMouse = (e) => {
if ((this.compareMode || 'slide') !== 'slide') return;
+2 -2
View File
@@ -2,9 +2,9 @@
* CropOverlayWidget — draggable/resizable crop box.
* Draws the green overlay on the CropCanvas during mouse interaction.
*/
import { RgthreeBaseWidget } from '../power_spline_editor/drawing_utils.js';
import { QQBaseWidget } from '../power_spline_editor/drawing_utils.js';
export class CropOverlayWidget extends RgthreeBaseWidget {
export class CropOverlayWidget extends QQBaseWidget {
constructor(name = "crop_overlay") {
super(name);
this.type = 'custom';
+2 -2
View File
@@ -5,10 +5,10 @@
import { app } from '../../../scripts/app.js';
import {
drawWidgetButton,
RgthreeBaseWidget,
QQBaseWidget,
} from '../power_spline_editor/drawing_utils.js';
export class PowerCropTopRowWidget extends RgthreeBaseWidget {
export class PowerCropTopRowWidget extends QQBaseWidget {
constructor(name = "PowerCropTopRow") {
super(name);
this.type = "custom";
+2 -2
View File
@@ -7,9 +7,9 @@
*/
import { app } from '../../../scripts/app.js';
import { RgthreeBaseWidget } from '../power_spline_editor/drawing_utils.js';
import { QQBaseWidget } from '../power_spline_editor/drawing_utils.js';
export class BoxCropWidget extends RgthreeBaseWidget {
export class BoxCropWidget extends QQBaseWidget {
constructor(name = "box_crop") {
super(name);
this.type = 'custom';
+2 -2
View File
@@ -7,9 +7,9 @@
*/
import { api } from '../../../scripts/api.js';
import { app } from '../../../scripts/app.js';
import { RgthreeBaseWidget, fitString } from '../power_spline_editor/drawing_utils.js';
import { QQBaseWidget, fitString } from '../power_spline_editor/drawing_utils.js';
export class PowerLoadVideoFileSelectorWidget extends RgthreeBaseWidget {
export class PowerLoadVideoFileSelectorWidget extends QQBaseWidget {
constructor(node) {
super("PowerLoadVideoFileSelector");
this.type = "custom";
+3 -3
View File
@@ -2,9 +2,9 @@
* Timeline Widget for video playback control
* Styled to match Power Spline Editor's timeline aesthetic
*/
import { RgthreeBaseWidget } from '../power_spline_editor/drawing_utils.js';
import { QQBaseWidget } from '../power_spline_editor/drawing_utils.js';
export class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget {
export class PowerLoadVideoTimelineWidget extends QQBaseWidget {
constructor(name = "PowerLoadVideoTimeline") {
super(name);
this.type = "custom";
@@ -602,7 +602,7 @@ export class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget {
/**
* Drag to seek along timeline
* Called by base class RgthreeBaseWidget when mouse is dragged over hit area
* Called by base class QQBaseWidget when mouse is dragged over hit area
* Uses direct position calculation for smooth real-time scrubbing
*/
dragSeek(event, pos, node) {
+2 -2
View File
@@ -3,9 +3,9 @@
* Similar to Power Spline Editor's top row with Refresh button and text inputs
*/
import { app } from '../../../scripts/app.js';
import { RgthreeBaseWidget } from '../power_spline_editor/drawing_utils.js';
import { QQBaseWidget } from '../power_spline_editor/drawing_utils.js';
export class PowerLoadVideoTopRowWidget extends RgthreeBaseWidget {
export class PowerLoadVideoTopRowWidget extends QQBaseWidget {
constructor(name = "PowerLoadVideoTopRow") {
super(name);
this.type = "custom";
@@ -1,6 +1,6 @@
// Import necessary modules
import { generatePreviewFromFirstImage } from "./image_utils.js";
import { rgthreeApi } from "./rgthree_api.js";
import { vidApi } from "./wanvid_api.js";
/**
* Batch Fetch Service - Handles bulk LoRA info fetching and preview generation
@@ -160,7 +160,6 @@ export class BatchFetchService {
if (isVideo) {
hasVideos = true;
videoCount++;
console.log(`🎥 Skipping video preview: ${image.url}`);
// Only skip if SKIP_VIDEO_PREVIEWS is enabled
if (!BatchFetchService.BATCH_CONFIG.SKIP_VIDEO_PREVIEWS) {
@@ -172,7 +171,6 @@ export class BatchFetchService {
}
if (hasVideos) {
console.log(`ℹ️ Found ${videoCount} video preview(s), ${BatchFetchService.BATCH_CONFIG.SKIP_VIDEO_PREVIEWS ? 'skipping' : 'including'} them`);
}
return {
@@ -197,7 +195,6 @@ export class BatchFetchService {
return response.ok;
} catch (error) {
console.log(`Error checking placeholder preview for ${loraName}:`, error);
return false;
}
}
@@ -210,7 +207,6 @@ export class BatchFetchService {
async createPlaceholderPreview(loraName) {
try {
const cleanLoraName = loraName.replace(/\.[^/.]+$/, ""); // Remove extension
console.log(`📋 Creating placeholder preview for ${loraName} from no_preview.jpg`);
// Fetch the no_preview.jpg placeholder
const placeholderResponse = await fetch('/wanvid/api/loras/preview?file=no_preview.jpg');
@@ -237,7 +233,6 @@ export class BatchFetchService {
}
const result = await saveResponse.json();
console.log(`✓ Created placeholder preview for ${loraName}:`, result);
} catch (error) {
console.error(`Failed to create placeholder preview for ${loraName}:`, error);
@@ -262,7 +257,6 @@ export class BatchFetchService {
updateProgress(overallProgress);
updateText(`Processing ${loraName} (${index + 1}/${total})`);
console.log(`[${index + 1}/${total}] Processing ${loraName}`);
// Stage 1: Fetch LoRA info with retry logic
const loraInfo = await this.fetchLoraInfoWithRetry(loraName, checkCancelled);
@@ -279,7 +273,6 @@ export class BatchFetchService {
const imageFilter = this.filterVideoImages(loraInfo);
if (!imageFilter.hasImages) {
console.log(`ℹ️ ${loraName}: Only video previews found (${imageFilter.videoCount} videos), skipping to prevent timeouts`);
return {
success: true,
loraName,
@@ -299,7 +292,6 @@ export class BatchFetchService {
};
updateText(`Generating preview for ${loraName} (${index + 1}/${total})`);
console.log(`[${index + 1}/${total}] Generating preview for ${loraName} (${imageFilter.filteredImages.length} images, skipped ${imageFilter.videoCount} videos)`);
previewResult = await this.generatePreviewsWithRetry(filteredLoraInfo, checkCancelled);
@@ -308,7 +300,6 @@ export class BatchFetchService {
throw new Error('Operation cancelled');
}
} else {
console.log(`ℹ No images found for ${loraName}, will create placeholder preview to prevent refetching`);
}
return {
@@ -324,7 +315,6 @@ export class BatchFetchService {
} catch (error) {
if (error.message === 'Operation cancelled') {
console.log(`[${loraName}] Processing cancelled`);
throw error;
}
@@ -449,7 +439,6 @@ export class BatchFetchService {
existing.push(loraName);
} else if (hasPlaceholder) {
withPlaceholderPreviews.push(loraName);
console.log(`ℹ️ ${loraName}: Has placeholder preview, already processed`);
} else {
missing.push(loraName);
}
@@ -457,7 +446,6 @@ export class BatchFetchService {
const processedTotal = existing.length + withPlaceholderPreviews.length;
console.log(`LoRA Preview Status: ${missing.length} missing, ${existing.length} have existing previews, ${withPlaceholderPreviews.length} have placeholder previews`);
switch (filterType) {
case 'missing':
@@ -514,7 +502,6 @@ export class BatchFetchService {
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
console.log(`[${loraName}] Fetching info from Civitai API (attempt ${attempt}/${maxRetries + 1})...`);
// Check for cancellation before starting API call
if (this.cancelFlag || checkCancelled()) {
@@ -525,7 +512,7 @@ export class BatchFetchService {
const adaptiveTimeout = baseTimeout * Math.pow(backoffMultiplier, attempt - 1);
// Use cancellable promise wrapper with adaptive timeout
const apiPromise = rgthreeApi.refreshSingleLoraInfo(loraName);
const apiPromise = vidApi.refreshSingleLoraInfo(loraName);
const result = await this.createCancellablePromise(
apiPromise,
adaptiveTimeout,
@@ -537,13 +524,11 @@ export class BatchFetchService {
throw new Error('Operation cancelled');
}
console.log(`[${loraName}] Successfully fetched info from Civitai (attempt ${attempt})`);
// Check if we got image data
if (result && result.length > 0) {
const loraInfo = result[0];
if (loraInfo.images && loraInfo.images.length > 0) {
console.log(`[${loraName}] Found ${loraInfo.images.length} images for preview generation`);
} else {
console.warn(`[${loraName}] No images found in fetched data for preview generation`);
}
@@ -558,7 +543,6 @@ export class BatchFetchService {
const isCancelled = error.message === 'Operation cancelled' || error.message === 'Operation timeout';
if (isCancelled) {
console.log(`[${loraName}] Fetch cancelled/timeout on attempt ${attempt}`);
throw new Error('Operation cancelled');
}
@@ -605,7 +589,6 @@ export class BatchFetchService {
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
console.log(`[${loraName}] Generating previews (attempt ${attempt}/${maxRetries + 1}) with params:`, { loraName, loraPath });
// Check for cancellation before starting preview generation
if (this.cancelFlag || checkCancelled()) {
@@ -628,7 +611,6 @@ export class BatchFetchService {
throw new Error('Operation cancelled');
}
console.log(`[${loraName}] Preview generation successful on attempt ${attempt}:`, previewResult);
return previewResult;
} catch (error) {
@@ -636,7 +618,6 @@ export class BatchFetchService {
const isCancelled = error.message === 'Operation cancelled' || error.message === 'Operation timeout';
if (isCancelled) {
console.log(`[${loraName}] Preview generation cancelled/timeout on attempt ${attempt}`);
throw new Error('Operation cancelled');
}
@@ -686,25 +667,14 @@ export class BatchFetchService {
const filteredData = await this.filterLorasByPreviewStatus(loras, hasPreviewCallback, filterType);
const { loras: lorasToFetch, skipped, stats } = filteredData;
// Log current configuration for performance tuning
const config = BatchFetchService.getConfig();
console.log(`Batch fetch configuration:`, {
batchSize: config.BATCH_SIZE,
interLoraDelay: config.INTER_LORA_DELAY,
interBatchDelay: config.INTER_BATCH_DELAY,
adaptiveDelay: config.ADAPTIVE_DELAY_ENABLED
});
console.log(`Starting ${stats.type} fetch: ${stats.fetching} LoRAs to fetch, ${stats.skipped} LoRAs skipped`);
if (lorasToFetch.length === 0) {
console.log(`No LoRAs to fetch for ${filterType} mode`);
return { success: true, message: 'No LoRAs to process', stats };
}
try {
console.log(`Starting PARALLEL batch processing of ${lorasToFetch.length} LoRAs`);
console.log(`Configuration: batch_size=${BatchFetchService.BATCH_CONFIG.BATCH_SIZE}, concurrent=${BatchFetchService.BATCH_CONFIG.MAX_CONCURRENT_REQUESTS}`);
let completedCount = 0;
let previewGeneratedCount = 0;
@@ -719,13 +689,11 @@ export class BatchFetchService {
for (let i = 0; i < lorasToFetch.length; i += batchSize) {
// Check for cancellation before processing each batch
if (this.cancelFlag || checkCancelled()) {
console.log('Batch operation cancelled by user');
updateText(`Operation cancelled. Processed ${completedCount}/${totalLoras} LoRAs`);
break;
}
const batch = lorasToFetch.slice(i, i + Math.min(batchSize, totalLoras - i));
console.log(`Processing batch ${Math.floor(i/batchSize) + 1} with ${batch.length} LoRAs in PARALLEL:`, batch);
try {
// Process the entire batch in parallel
@@ -757,9 +725,7 @@ export class BatchFetchService {
batchNoImages++;
if (result.skippedVideos > 0) {
batchVideosSkipped += result.skippedVideos;
console.log(`ℹ️ ${result.loraName}: Skipped ${result.skippedVideos} video preview(s) to prevent timeouts`);
} else {
console.log(`ℹ️ ${result.loraName}: Info fetched successfully, but no preview images available`);
}
// Queue for placeholder preview creation (even if cancelled)
@@ -775,18 +741,15 @@ export class BatchFetchService {
// Create placeholder previews for LoRAs that need them (even if operation was cancelled)
if (placeholderPreviewQueue.length > 0) {
console.log(`Creating placeholder previews for ${placeholderPreviewQueue.length} LoRAs...`);
for (const loraName of placeholderPreviewQueue) {
try {
await this.createPlaceholderPreview(loraName);
console.log(`✓ Created placeholder preview for ${loraName}`);
} catch (placeholderError) {
console.warn(`⚠ Failed to create placeholder preview for ${loraName}:`, placeholderError);
}
}
}
console.log(`Batch ${Math.floor(i/batchSize) + 1} completed: ${batchInfoFetched} info fetched, ${batchPreviewsGenerated} previews generated, ${batchNoImages} no images, ${batchVideosSkipped} videos skipped, ${batchFailures} failures`);
// Accumulate counts
videosSkippedCount += batchVideosSkipped;
@@ -794,7 +757,6 @@ export class BatchFetchService {
} catch (error) {
if (error.message === 'Operation cancelled') {
console.log('Batch processing cancelled');
break;
}
console.error(`Batch ${Math.floor(i/batchSize) + 1} failed:`, error);
@@ -803,13 +765,11 @@ export class BatchFetchService {
// Check for cancellation between batches
if (this.cancelFlag || checkCancelled()) {
console.log('Batch operation cancelled between batches');
break;
}
// Short delay between batches for API safety (cancellable)
if (i + batchSize < lorasToFetch.length) {
console.log(`Batch ${Math.floor(i/batchSize) + 1} completed, short delay before next batch...`);
try {
await this.createCancellableDelay(
BatchFetchService.BATCH_CONFIG.INTER_BATCH_DELAY,
@@ -817,7 +777,6 @@ export class BatchFetchService {
);
} catch (delayError) {
if (delayError.message === 'Operation cancelled') {
console.log('Inter-batch delay cancelled, stopping batch processing');
break;
}
}
@@ -859,7 +818,6 @@ export class BatchFetchService {
}
updateText(completionMessage);
console.log(`Batch processing ${this.cancelFlag ? 'cancelled' : 'completed'}: ${completionMessage}`);
return {
success: !this.cancelFlag,
@@ -879,7 +837,6 @@ export class BatchFetchService {
} catch (error) {
if (error.message === 'Operation cancelled') {
console.log('Batch fetch operation cancelled by user');
updateText('Operation cancelled successfully');
return {
success: false,
@@ -909,7 +866,6 @@ export class BatchFetchService {
* Cancel the current batch operation aggressively
*/
cancel() {
console.log('Cancelling batch operation aggressively...');
this.cancelFlag = true;
// Abort any ongoing requests
@@ -928,7 +884,6 @@ export class BatchFetchService {
...BatchFetchService.BATCH_CONFIG,
...newConfig
};
console.log('Batch fetch configuration updated:', BatchFetchService.BATCH_CONFIG);
}
/**
@@ -947,7 +902,6 @@ export class BatchFetchService {
* @returns {Promise<Object>} Comprehensive statistics object
*/
async gatherComprehensiveStats(hasPreviewCallback, allLoras) {
console.log('📊 Gathering comprehensive LoRA statistics...');
const stats = {
totalLoRAs: 0,
@@ -1000,7 +954,6 @@ export class BatchFetchService {
stats.fileTypes[extension] = (stats.fileTypes[extension] || 0) + 1;
}
console.log('✓ LoRA statistics gathered successfully');
return stats;
} catch (error) {
+90 -90
View File
@@ -3,13 +3,13 @@ import { createElement as $el, empty, appendChildren, getClosestOrSelf, query, q
import { logoCivitai, link, pencilColored, diskColored, dotdotdot } from "./svgs.js";
import { LORA_INFO_SERVICE } from "./model_info_service.js";
import { generateId, injectCss } from "./shared_utils.js";
import { rgthree } from "../rgthree/common/rgthree.js";
import { qq } from "../qq_lib/common/qq.js";
import { proxyImageUrl } from "./image_utils.js";
class WanInfoDialog extends WanDialog {
constructor(file) {
const dialogOptions = {
class: "rgthree-info-dialog",
class: "qq-info-dialog",
title: `<h2>Loading...</h2>`,
content: "<center>Loading..</center>",
onBeforeClose: () => {
@@ -80,28 +80,28 @@ class WanInfoDialog extends WanDialog {
}
/* Info dialog specific styling - match power lora UI */
.rgthree-info-dialog {
.qq-info-dialog {
width: 90vw;
max-width: 960px;
}
.rgthree-info-dialog .rgthree-info-area {
.qq-info-dialog .qq-info-area {
list-style: none;
padding: 0;
margin: 0;
display: flex;
}
.rgthree-info-dialog .rgthree-info-area > li {
.qq-info-dialog .qq-info-area > li {
display: inline-flex;
margin: 0;
vertical-align: top;
}
.rgthree-info-dialog .rgthree-info-area > li + li {
.qq-info-dialog .qq-info-area > li + li {
margin-left: 4px;
}
.rgthree-info-dialog .rgthree-info-area > li:not(.-link) + li.-link {
.qq-info-dialog .qq-info-area > li:not(.-link) + li.-link {
margin-left: auto;
}
.rgthree-info-dialog .rgthree-info-area > li.rgthree-info-tag > * {
.qq-info-dialog .qq-info-area > li.qq-info-tag > * {
min-height: 20px;
border-radius: 0px;
line-height: 1;
@@ -118,71 +118,71 @@ class WanInfoDialog extends WanDialog {
align-items: center;
border: 1px solid #333;
}
.rgthree-info-dialog .rgthree-info-area > li.-type > * {
.qq-info-dialog .qq-info-area > li.-type > * {
background: #2a2a2a;
color: #89B;
}
.rgthree-info-dialog .rgthree-info-area > li.rgthree-info-tag > * > svg {
.qq-info-dialog .qq-info-area > li.qq-info-tag > * > svg {
width: 16px;
height: 16px;
}
.rgthree-info-dialog .rgthree-info-area > li.rgthree-info-tag > *:empty {
.qq-info-dialog .qq-info-area > li.qq-info-tag > *:empty {
display: none;
}
/* Table styling - match power lora UI */
.rgthree-info-dialog .rgthree-info-table {
.qq-info-dialog .qq-info-table {
border-collapse: collapse;
margin: 8px 0px;
width: 100%;
font-size: 11px;
}
.rgthree-info-dialog .rgthree-info-table td {
.qq-info-dialog .qq-info-table td {
position: relative;
border: 1px solid #333;
padding: 0;
vertical-align: top;
background: #1a1a1a;
}
.rgthree-info-dialog .rgthree-info-table td:first-child {
.qq-info-dialog .qq-info-table td:first-child {
background: #222;
width: 10px;
}
.rgthree-info-dialog .rgthree-info-table td:first-child > *:first-child {
.qq-info-dialog .qq-info-table td:first-child > *:first-child {
white-space: nowrap;
padding-right: 24px;
}
.rgthree-info-dialog .rgthree-info-table td:first-child small {
.qq-info-dialog .qq-info-table td:first-child small {
display: block;
margin-top: 2px;
opacity: 0.75;
}
.rgthree-info-dialog .rgthree-info-table td:first-child small > [data-action] {
.qq-info-dialog .qq-info-table td:first-child small > [data-action] {
text-decoration: underline;
cursor: pointer;
}
.rgthree-info-dialog .rgthree-info-table td:first-child small > [data-action]:hover {
.qq-info-dialog .qq-info-table td:first-child small > [data-action]:hover {
text-decoration: none;
}
.rgthree-info-dialog .rgthree-info-table td a,
.rgthree-info-dialog .rgthree-info-table td a:hover,
.rgthree-info-dialog .rgthree-info-table td a:visited {
.qq-info-dialog .qq-info-table td a,
.qq-info-dialog .qq-info-table td a:hover,
.qq-info-dialog .qq-info-table td a:visited {
color: #89B;
}
.rgthree-info-dialog .rgthree-info-table td svg {
.qq-info-dialog .qq-info-table td svg {
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
}
.rgthree-info-dialog .rgthree-info-table td svg.logo-civitai {
.qq-info-dialog .qq-info-table td svg.logo-civitai {
margin-right: 0.2em;
}
.rgthree-info-dialog .rgthree-info-table td > *:first-child {
.qq-info-dialog .qq-info-table td > *:first-child {
display: block;
padding: 4px 8px;
}
.rgthree-info-dialog .rgthree-info-table td > input,
.rgthree-info-dialog .rgthree-info-table td > textarea {
.qq-info-dialog .qq-info-table td > input,
.qq-info-dialog .qq-info-table td > textarea {
padding: 4px 6px;
border: 1px solid #333;
box-shadow: none;
@@ -194,16 +194,16 @@ class WanInfoDialog extends WanDialog {
resize: vertical;
width: 100%;
}
.rgthree-info-dialog .rgthree-info-table td > input:focus,
.rgthree-info-dialog .rgthree-info-table td > textarea:focus {
.qq-info-dialog .qq-info-table td > input:focus,
.qq-info-dialog .qq-info-table td > textarea:focus {
border-color: #555;
background: #2a2a2a;
outline: none;
}
.rgthree-info-dialog .rgthree-info-table tr[data-field-name=userNote] td > span:first-child {
.qq-info-dialog .qq-info-table tr[data-field-name=userNote] td > span:first-child {
white-space: pre;
}
.rgthree-info-dialog .rgthree-info-table td .-help {
.qq-info-dialog .qq-info-table td .-help {
border: 1px solid #555;
position: absolute;
right: 4px;
@@ -219,12 +219,12 @@ class WanInfoDialog extends WanDialog {
cursor: help;
background: #222;
}
.rgthree-info-dialog .rgthree-info-table td .-help::before {
.qq-info-dialog .qq-info-table td .-help::before {
content: "?";
}
/* Button styling - match power lora UI */
.rgthree-button {
.qq-button {
position: relative;
cursor: pointer;
border: 1px solid #000;
@@ -245,20 +245,20 @@ class WanInfoDialog extends WanDialog {
align-items: center;
justify-content: center;
}
.rgthree-button:hover {
.qq-button:hover {
background: #2a2a2a;
}
.rgthree-button:active {
.qq-button:active {
background: #333;
}
.rgthree-info-dialog .rgthree-info-table .rgthree-button[data-action=fetch-civitai] {
.qq-info-dialog .qq-info-table .qq-button[data-action=fetch-civitai] {
font-size: inherit;
padding: 4px 8px;
margin: 2px;
}
/* Edit button styling */
.rgthree-button-edit {
.qq-button-edit {
background: none;
border: none;
cursor: pointer;
@@ -271,22 +271,22 @@ class WanInfoDialog extends WanDialog {
align-items: center;
justify-content: center;
}
.rgthree-button-edit:hover {
.qq-button-edit:hover {
opacity: 1;
color: #89B;
}
.rgthree-info-dialog .rgthree-info-table tr.editable button svg + svg {
.qq-info-dialog .qq-info-table tr.editable button svg + svg {
display: none;
}
.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg {
.qq-info-dialog .qq-info-table tr.editable.-qq-editing button svg {
display: none;
}
.rgthree-info-dialog .rgthree-info-table tr.editable.-rgthree-editing button svg + svg {
.qq-info-dialog .qq-info-table tr.editable.-qq-editing button svg + svg {
display: inline-block;
}
/* Trained words styling - match power lora UI */
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list {
list-style: none;
padding: 2px 4px;
margin: 0;
@@ -296,7 +296,7 @@ class WanInfoDialog extends WanDialog {
max-height: 15vh;
overflow: auto;
}
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list > li {
display: inline-flex;
margin: 1px;
vertical-align: top;
@@ -317,21 +317,21 @@ class WanInfoDialog extends WanDialog {
white-space: nowrap;
max-width: 183px;
}
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li:hover {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list > li:hover {
background: #333;
}
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li > svg {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list > li > svg {
width: auto;
height: 1em;
}
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li > span {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list > li > span {
padding-left: 0.3em;
padding-right: 0.3em;
padding-bottom: 0.1em;
text-overflow: ellipsis;
overflow: hidden;
}
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li > small {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list > li > small {
align-self: stretch;
display: flex;
align-items: center;
@@ -339,13 +339,13 @@ class WanInfoDialog extends WanDialog {
padding: 0 0.3em;
background: rgba(0, 0, 0, 0.2);
}
.rgthree-info-dialog .rgthree-info-table td > ul.rgthree-info-trained-words-list > li.-rgthree-is-selected {
.qq-info-dialog .qq-info-table td > ul.qq-info-trained-words-list > li.-qq-is-selected {
background: #444;
color: #89B;
}
/* Images styling - match power lora UI */
.rgthree-info-dialog .rgthree-info-images {
.qq-info-dialog .qq-info-images {
list-style: none;
padding: 0;
margin: 0;
@@ -354,7 +354,7 @@ class WanInfoDialog extends WanDialog {
flex-direction: row;
overflow: auto;
}
.rgthree-info-dialog .rgthree-info-images > li {
.qq-info-dialog .qq-info-images > li {
scroll-snap-align: start;
max-width: 90%;
flex: 0 0 auto;
@@ -369,15 +369,15 @@ class WanInfoDialog extends WanDialog {
position: relative;
border: 1px solid #333;
}
.rgthree-info-dialog .rgthree-info-images > li figure {
.qq-info-dialog .qq-info-images > li figure {
margin: 0;
position: static;
}
.rgthree-info-dialog .rgthree-info-images > li figure video,
.rgthree-info-dialog .rgthree-info-images > li figure img {
.qq-info-dialog .qq-info-images > li figure video,
.qq-info-dialog .qq-info-images > li figure img {
max-height: 45vh;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption {
.qq-info-dialog .qq-info-images > li figure figcaption {
position: absolute;
left: 0;
width: 100%;
@@ -389,7 +389,7 @@ class WanInfoDialog extends WanDialog {
transform: translateY(50px);
transition: all 0.25s ease-in-out;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption > span {
.qq-info-dialog .qq-info-images > li figure figcaption > span {
display: inline-block;
padding: 2px 4px;
margin: 1px;
@@ -397,7 +397,7 @@ class WanInfoDialog extends WanDialog {
border: 1px solid #333;
word-break: break-word;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption > span label {
.qq-info-dialog .qq-info-images > li figure figcaption > span label {
display: inline;
padding: 0;
margin: 0;
@@ -405,45 +405,45 @@ class WanInfoDialog extends WanDialog {
pointer-events: none;
user-select: none;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption > span a {
.qq-info-dialog .qq-info-images > li figure figcaption > span a {
color: #89B;
text-decoration: underline;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption > span a:hover {
.qq-info-dialog .qq-info-images > li figure figcaption > span a:hover {
text-decoration: none;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption > span a svg {
.qq-info-dialog .qq-info-images > li figure figcaption > span a svg {
height: 10px;
margin-left: 4px;
fill: currentColor;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption:empty {
.qq-info-dialog .qq-info-images > li figure figcaption:empty {
text-align: center;
}
.rgthree-info-dialog .rgthree-info-images > li figure figcaption:empty::before {
.qq-info-dialog .qq-info-images > li figure figcaption:empty::before {
content: "No data.";
}
.rgthree-info-dialog .rgthree-info-images > li:hover figure figcaption {
.qq-info-dialog .qq-info-images > li:hover figure figcaption {
opacity: 1;
transform: translateY(0px);
}
/* Constrain long prompt text (positive/negative) so the caption
doesn't grow past the image and clip off the Copy+ button. */
.rgthree-info-dialog .rgthree-info-images figure figcaption > span.-info-field-long {
.qq-info-dialog .qq-info-images figure figcaption > span.-info-field-long {
display: block;
max-height: 8.8em; /* ~8 lines at 11px font */
overflow-y: auto;
overscroll-behavior: contain;
}
.rgthree-info-dialog .rgthree-info-images figure figcaption > span.-info-field-long > label {
.qq-info-dialog .qq-info-images figure figcaption > span.-info-field-long > label {
position: sticky;
left: 0;
}
.rgthree-info-dialog .rgthree-info-images figure figcaption > span.-info-field-long::-webkit-scrollbar {
.qq-info-dialog .qq-info-images figure figcaption > span.-info-field-long::-webkit-scrollbar {
width: 6px;
}
.rgthree-info-dialog .rgthree-info-images figure figcaption > span.-info-field-long::-webkit-scrollbar-thumb {
.qq-info-dialog .qq-info-images figure figcaption > span.-info-field-long::-webkit-scrollbar-thumb {
background: #444;
}
`;
@@ -503,7 +503,7 @@ class WanInfoDialog extends WanDialog {
// Add to success message
const itemTypeName = itemType === 'checkpoints' ? 'Model' : 'LoRA';
rgthree.showMessage({
qq.showMessage({
id: "refresh-info-" + generateId(4),
type: "success",
message: `${itemTypeName} information refreshed successfully (preview image generated)`,
@@ -511,7 +511,7 @@ class WanInfoDialog extends WanDialog {
});
} catch (previewError) {
// Still show success message for the refresh itself
rgthree.showMessage({
qq.showMessage({
id: "refresh-info-" + generateId(4),
type: "success",
message: "LoRA information refreshed successfully",
@@ -520,7 +520,7 @@ class WanInfoDialog extends WanDialog {
}
} else {
// Show success message
rgthree.showMessage({
qq.showMessage({
id: "refresh-info-" + generateId(4),
type: "success",
message: "LoRA information refreshed successfully",
@@ -529,7 +529,7 @@ class WanInfoDialog extends WanDialog {
}
} catch (error) {
// Show error message
rgthree.showMessage({
qq.showMessage({
id: "refresh-info-error-" + generateId(4),
type: "error",
message: "Failed to refresh LoRA information",
@@ -564,14 +564,14 @@ class WanInfoDialog extends WanDialog {
if (positivePrompt) {
await navigator.clipboard.writeText(positivePrompt);
rgthree.showMessage({
qq.showMessage({
id: "copy-positive-" + generateId(4),
type: "success",
message: "Positive prompt copied to clipboard",
timeout: 3000,
});
} else {
rgthree.showMessage({
qq.showMessage({
id: "copy-positive-error-" + generateId(4),
type: "error",
message: "No positive prompt found to copy",
@@ -580,10 +580,10 @@ class WanInfoDialog extends WanDialog {
}
}
else if (action === "copy-trained-words") {
const selected = queryAll(".-rgthree-is-selected", target.closest("tr"));
const selected = queryAll(".-qq-is-selected", target.closest("tr"));
const text = selected.map((el) => el.getAttribute("data-word")).join(", ");
await navigator.clipboard.writeText(text);
rgthree.showMessage({
qq.showMessage({
id: "copy-trained-words-" + generateId(4),
type: "success",
message: `Successfully copied ${selected.length} key word${selected.length === 1 ? "" : "s"}.`,
@@ -591,7 +591,7 @@ class WanInfoDialog extends WanDialog {
});
}
else if (action === "toggle-trained-word") {
target === null || target === void 0 ? void 0 : target.classList.toggle("-rgthree-is-selected");
target === null || target === void 0 ? void 0 : target.classList.toggle("-qq-is-selected");
const tr = target.closest("tr");
if (tr) {
const span = query("td:first-child > *", tr);
@@ -599,7 +599,7 @@ class WanInfoDialog extends WanDialog {
if (!small) {
small = $el("small", { parent: span });
}
const num = queryAll(".-rgthree-is-selected", tr).length;
const num = queryAll(".-qq-is-selected", tr).length;
small.innerHTML = num
? `${num} selected | <span role="button" data-action="copy-trained-words">Copy</span>`
: "";
@@ -611,7 +611,7 @@ class WanInfoDialog extends WanDialog {
const input = td.querySelector("input,textarea");
if (!input) {
const fieldName = tr.dataset["fieldName"];
tr.classList.add("-rgthree-editing");
tr.classList.add("-qq-editing");
const isTextarea = fieldName === "userNote";
const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, {
value: td.textContent,
@@ -647,15 +647,15 @@ class WanInfoDialog extends WanDialog {
const info = this.modelInfo || {};
const civitaiLink = (_a = info.links) === null || _a === void 0 ? void 0 : _a.find((i) => i.includes("civitai.com/models"));
const html = `
<ul class="rgthree-info-area">
<li title="Type" class="rgthree-info-tag -type -type-${(info.type || "").toLowerCase()}"><span>${info.type || ""}</span></li>
<li title="Base Model" class="rgthree-info-tag -basemodel -basemodel-${(info.baseModel || "").toLowerCase()}"><span>${info.baseModel || ""}</span></li>
<li class="rgthree-info-menu" stub="menu"></li>
<ul class="qq-info-area">
<li title="Type" class="qq-info-tag -type -type-${(info.type || "").toLowerCase()}"><span>${info.type || ""}</span></li>
<li title="Base Model" class="qq-info-tag -basemodel -basemodel-${(info.baseModel || "").toLowerCase()}"><span>${info.baseModel || ""}</span></li>
<li class="qq-info-menu" stub="menu"></li>
${""}
</ul>
<table class="rgthree-info-table">
${infoTableRow("File", `${info.file || ""} <button class="rgthree-button" data-action="refresh-info" style="margin-left: 8px; padding: 2px 6px; font-size: 10px;">↻ Refresh</button>`)}
<table class="qq-info-table">
${infoTableRow("File", `${info.file || ""} <button class="qq-button" data-action="refresh-info" style="margin-left: 8px; padding: 2px 6px; font-size: 10px;">↻ Refresh</button>`)}
${infoTableRow("Hash (sha256)", info.sha256 || "")}
${civitaiLink
? infoTableRow("Civitai", `<a href="${civitaiLink}" target="_blank">${logoCivitai}View on Civitai</a>`)
@@ -664,7 +664,7 @@ class WanInfoDialog extends WanDialog {
: ((_e = (_d = info.raw) === null || _d === void 0 ? void 0 : _d.civitai) === null || _e === void 0 ? void 0 : _e.error)
? infoTableRow("Civitai", (_g = (_f = info.raw) === null || _f === void 0 ? void 0 : _f.civitai) === null || _g === void 0 ? void 0 : _g.error)
: !((_h = info.raw) === null || _h === void 0 ? void 0 : _h.civitai)
? infoTableRow("Civitai", `<button class="rgthree-button" data-action="fetch-civitai">Fetch info from civitai</button>`)
? infoTableRow("Civitai", `<button class="qq-button" data-action="fetch-civitai">Fetch info from civitai</button>`)
: ""}
${infoTableRow("Name", info.name || ((_k = (_j = info.raw) === null || _j === void 0 ? void 0 : _j.metadata) === null || _k === void 0 ? void 0 : _k.ss_output_name) || "", "The name for display.", "name")}
@@ -688,7 +688,7 @@ class WanInfoDialog extends WanDialog {
</table>
<ul class="rgthree-info-images">${(_y = (_x = info.images) === null || _x === void 0 ? void 0 : _x.map((img) => {
<ul class="qq-info-images">${(_y = (_x = info.images) === null || _x === void 0 ? void 0 : _x.map((img) => {
// Use proxy for Civitai images to avoid CSP violations
const proxiedUrl = proxyImageUrl(img.url);
return `
@@ -698,7 +698,7 @@ class WanInfoDialog extends WanDialog {
: `<img src="${proxiedUrl}" />`}
<figcaption><!--
-->${imgInfoField("", img.civitaiUrl
? `<a href="${img.civitaiUrl}" target="_blank">civitai${link}</a> ${img.positive ? `<button class="rgthree-button" data-action="copy-positive" data-positive="${encodeURIComponent(img.positive)}" style="margin-left: 4px; padding: 1px 4px; font-size: 9px;">Copy+</button>` : ""}`
? `<a href="${img.civitaiUrl}" target="_blank">civitai${link}</a> ${img.positive ? `<button class="qq-button" data-action="copy-positive" data-positive="${encodeURIComponent(img.positive)}" style="margin-left: 4px; padding: 1px 4px; font-size: 9px;">Copy+</button>` : ""}`
: undefined)}<!--
-->${imgInfoField("seed", img.seed)}<!--
-->${imgInfoField("steps", img.steps)}<!--
@@ -713,7 +713,7 @@ class WanInfoDialog extends WanDialog {
}).join("")) !== null && _y !== void 0 ? _y : ""}</ul>
`;
const div = $el("div", { html });
if (rgthree.isDevMode()) {
if (qq.isDevMode()) {
setAttributes(query('[stub="menu"]', div), {
children: [
// Menu button functionality would go here
@@ -742,15 +742,15 @@ function infoTableRow(name, value, help = "", editableFieldName = "") {
<td><span>${name} ${help ? `<span class="-help" title="${help}">ℹ️</span>` : ""}<span></td>
<td ${editableFieldName ? "" : 'colspan="2"'}>${String(value).startsWith("<") ? value : `<span>${value}<span>`}</td>
${editableFieldName
? `<td style="width: 24px;"><button class="rgthree-button-edit" data-action="edit-row">${pencilColored}${diskColored}</button></td>`
? `<td style="width: 24px;"><button class="qq-button-edit" data-action="edit-row">${pencilColored}${diskColored}</button></td>`
: ""}
</tr>`;
}
function getTrainedWordsMarkup(words) {
let markup = `<ul class="rgthree-info-trained-words-list">`;
let markup = `<ul class="qq-info-trained-words-list">`;
for (const wordData of words || []) {
markup += `<li title="${wordData.word}" data-word="${wordData.word}" class="rgthree-info-trained-words-list-item" data-action="toggle-trained-word">
markup += `<li title="${wordData.word}" data-word="${wordData.word}" class="qq-info-trained-words-list-item" data-action="toggle-trained-word">
<span>${wordData.word}</span>
${wordData.civitai ? logoCivitai : ""}
${wordData.count != null ? `<small>${wordData.count}</small>` : ""}
@@ -778,7 +778,7 @@ function saveEditableRow(info, tr, saving = true) {
LORA_INFO_SERVICE.savePartialInfo(info.file, { [fieldName]: newValue }, { generatePreview: true });
modified = true;
}
tr.classList.remove("-rgthree-editing");
tr.classList.remove("-qq-editing");
const td = query("td:nth-child(2)", tr);
appendChildren(empty(td), [$el("span", { text: newValue })]);
return modified;
+1 -1
View File
@@ -3,7 +3,7 @@
* Handles image extraction, scaling, and preview generation for LoRA files
*/
import { rgthreeApi } from "./rgthree_api.js";
import { vidApi } from "./wanvid_api.js";
/**
* Convert external Civitai image URLs to proxy URLs to avoid CSP violations
+159 -531
View File
@@ -1,196 +1,168 @@
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) {
console.log("showCustomLoraMenu called.");
// Remove any existing custom menu to prevent duplicates
const existingMenu = document.getElementById('custom-lora-menu');
if (existingMenu) {
existingMenu.remove();
// Slot context menus for the Power Lora Loader family.
//
// Newer ComfyUI frontends call `new LiteGraph.ContextMenu(options, ...)`
// unconditionally with whatever `node.getSlotMenuOptions(slot)` returns, so
// these handlers MUST return a proper options array (returning false/undefined
// crashes with "t.some is not a function"). ComfyUI renders the menu itself.
const I_BLACK = '⚫';
const I_GREEN = '🟢';
const I_WHITE = '⚪';
const I_BLUE = '🔵';
const I_RED = '🔴';
const I_PURPLE = '🟣';
const I_INFO = 'ℹ️';
const I_UP = '⬆️';
const I_DOWN = '⬇️';
const I_PENCIL = '✏️';
const I_TRASH = '🗑️';
// Path separators without backslash literals (transport-safe)
const FWD_SLASH = String.fromCharCode(47); // forward slash
const BACK_SLASH = String.fromCharCode(92); // back slash
function setDirty(node) {
node.setDirtyCanvas(true, true);
}
function moveLoraWidget(node, widget, offset) {
const loraWidgets = node.widgets.filter(w => w.name?.startsWith("lora_"));
const index = loraWidgets.indexOf(widget);
const other = loraWidgets[index + offset];
if (!other) return;
const otherIndex = node.widgets.indexOf(other);
const myIndex = node.widgets.indexOf(widget);
node.widgets[otherIndex] = widget;
node.widgets[myIndex] = other;
node.setDirtyCanvas(true, true);
}
// Builds the LiteGraph menu options for a lora slot (row right-click) or the
// low-variant icon right-click. Returns null when the slot is not one of ours.
function buildSlotMenuItems(slot, node) {
const dirty = () => setDirty(node);
// --- Low variant icon right-click menu ---
if (slot?.output?.type === "LOW_VARIANT_ICON") {
const widget = slot.widget;
const hasLowVariant = widget.value.is_low && widget.value.low_variant_name;
const isBothMode = widget.value.use_for_both;
const isLowOnlyMode = widget.value.low_only;
const isHighOnlyMode = widget.value.high_only;
const setMode = (both, low, high) => () => {
widget.value.use_for_both = both;
widget.value.low_only = low;
widget.value.high_only = high;
dirty();
};
if (hasLowVariant || isBothMode || isLowOnlyMode || isHighOnlyMode) {
let displayText;
if (isBothMode) {
displayText = widget.value.lora + " (both)";
} else if (isLowOnlyMode) {
displayText = widget.value.lora + " (low only)";
} else if (isHighOnlyMode) {
displayText = widget.value.lora + " (high only)";
} else {
displayText = widget.value.low_variant_name;
}
const pathSplit = new RegExp('[' + FWD_SLASH + BACK_SLASH + ']');
const fileName = displayText.includes(FWD_SLASH) || displayText.includes(BACK_SLASH)
? displayText.split(pathSplit).pop()
: displayText;
const items = [{ content: fileName, disabled: true }, null];
if (hasLowVariant) {
items.push({
content: (widget.value.low_active ? I_BLACK : I_GREEN) + " Toggle " + (widget.value.low_active ? 'Off' : 'On'),
callback: () => {
widget.value.low_active = !widget.value.low_active;
dirty();
},
}, null);
items.push({ content: I_INFO + ' Show Info', callback: () => widget.showLoraInfoDialog('low') });
} else if (isBothMode) {
items.push(
{ content: I_WHITE + ' Disable', callback: setMode(false, false, false) },
{ content: I_BLUE + ' Switch to Low only', callback: setMode(false, true, false) },
{ content: I_RED + ' Switch to High only', callback: setMode(false, false, true) },
);
} else if (isLowOnlyMode) {
items.push(
{ content: I_WHITE + ' Disable', callback: setMode(false, false, false) },
{ content: I_PURPLE + ' Switch to Both', callback: setMode(true, false, false) },
{ content: I_RED + ' Switch to High only', callback: setMode(false, false, true) },
);
} else if (isHighOnlyMode) {
items.push(
{ content: I_WHITE + ' Disable', callback: setMode(false, false, false) },
{ content: I_PURPLE + ' Switch to Both', callback: setMode(true, false, false) },
{ content: I_BLUE + ' Switch to Low only', callback: setMode(false, true, false) },
);
}
return items;
}
// No low variant: show mode options
return [
{ content: I_PURPLE + ' Enable for both', callback: setMode(true, false, false) },
{ content: I_BLUE + ' Low only', callback: setMode(false, true, false) },
{ content: I_RED + ' High only', callback: setMode(false, false, true) },
];
}
const menu = document.createElement('div');
menu.id = 'custom-lora-menu';
menu.className = 'litegraph litecontextmenu litemenubar-panel';
menu.style.cssText = `
position: absolute !important;
left: ${position.x}px !important;
top: ${position.y}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: 140px !important;
display: block !important;
font-family: Arial, sans-serif !important;
font-size: 11px !important;
`;
// --- LoRA row right-click menu ---
if (slot?.output?.type === "LORA_WIDGET" || slot?.widget?.name?.startsWith("lora_")) {
const widget = slot.widget;
const loraWidgets = node.widgets.filter(w => w.name?.startsWith("lora_"));
const index = loraWidgets.indexOf(widget);
const canMoveUp = index > 0;
const canMoveDown = index >= 0 && index < loraWidgets.length - 1;
// The Simple loader (strengthSplit === false) has no toggle entry.
const isV2 = widget.strengthSplit !== false;
const loraWidgets = node.widgets.filter(w => w.name?.startsWith("lora_"));
const loraIndex = loraWidgets.indexOf(widget);
const canMoveUp = loraIndex > 0;
const canMoveDown = loraIndex < loraWidgets.length - 1;
const items = [
{ content: I_INFO + ' Show Info', callback: () => widget.showLoraInfoDialog('high') },
null,
];
// Helper to create menu item
const createMenuItem = (icon, 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);
document.removeEventListener('contextmenu', hideMenu);
document.removeEventListener('mousedown', hideMenu);
};
if (isV2) {
items.push({
content: (widget.value.on ? I_BLACK : I_GREEN) + " Toggle " + (widget.value.on ? 'Off' : 'On'),
callback: () => {
widget.value.on = !widget.value.on;
dirty();
},
}, null);
}
const iconSpan = document.createElement('span');
iconSpan.textContent = icon;
iconSpan.style.cssText = `
font-size: 12px !important;
width: 16px !important;
text-align: center !important;
`;
const textSpan = document.createElement('span');
textSpan.textContent = text;
item.appendChild(iconSpan);
item.appendChild(textSpan);
return item;
};
// Helper to create separator
const createSeparator = () => {
const separator = document.createElement('div');
separator.style.cssText = `
height: 1px !important;
background-color: #333 !important;
margin: 2px 0 !important;
`;
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',
items.push(
{ content: I_UP + ' Move Up', disabled: !canMoveUp, callback: () => moveLoraWidget(node, widget, -1) },
{ content: I_DOWN + ' Move Down', disabled: !canMoveDown, callback: () => moveLoraWidget(node, widget, 1) },
{
content: I_PENCIL + ' Rename',
callback: () => renameLoRAFiles(widget.value.lora, 'high', null, null, node, widget),
},
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);
});
};
{
content: I_TRASH + ' Remove',
callback: () => {
removeArrayItem(node.widgets, widget);
const computed = node.computeSize();
node.size[1] = Math.max(node._tempHeight || 15, computed[1]);
node.setDirtyCanvas(true, true);
},
},
);
return items;
}
// Add menu items
menu.appendChild(createMenuItem('ℹ️', 'Show Info', () => {
// For the main LoRA widget context menu, always show the high variant info
widget.showLoraInfoDialog('high');
}));
menu.appendChild(createSeparator());
menu.appendChild(createMenuItem(
widget.value.on ? '⚫' : '🟢',
widget.value.on ? 'Toggle Off' : 'Toggle On',
() => {
widget.value.on = !widget.value.on;
node.setDirtyCanvas(true, true);
}
));
menu.appendChild(createSeparator());
menu.appendChild(createMenuItem('⬆️', 'Move Up', () => {
const index = node.widgets.indexOf(widget);
const otherIndex = node.widgets.indexOf(loraWidgets[loraIndex - 1]);
const temp = node.widgets[otherIndex];
node.widgets[otherIndex] = widget;
node.widgets[index] = temp;
node.setDirtyCanvas(true, true);
}, !canMoveUp));
menu.appendChild(createMenuItem('⬇️', 'Move Down', () => {
const index = node.widgets.indexOf(widget);
const otherIndex = node.widgets.indexOf(loraWidgets[loraIndex + 1]);
const temp = node.widgets[otherIndex];
node.widgets[otherIndex] = widget;
node.widgets[index] = temp;
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.setDirtyCanvas(true, true);
}));
document.body.appendChild(menu);
console.log("LoRA menu appended to body, position:", position);
// Prevent menu close when interacting with menu
menu.onclick = (e) => e.stopPropagation();
menu.onmousedown = (e) => e.stopPropagation();
// Hide menu when clicking outside
const hideMenu = (e) => {
if (menu && !menu.contains(e.target)) {
console.log("Hiding LoRA menu");
menu.remove();
document.removeEventListener('click', hideMenu);
document.removeEventListener('contextmenu', hideMenu);
document.removeEventListener('mousedown', hideMenu);
}
};
// Add listeners after a short delay to prevent immediate closure
setTimeout(() => {
document.addEventListener('click', hideMenu);
document.addEventListener('contextmenu', hideMenu);
document.addEventListener('mousedown', hideMenu);
}, 100);
return null;
}
export function getLoraSlotInPosition(canvasX, canvasY) {
@@ -207,10 +179,9 @@ export function getLoraSlotInPosition(canvasX, canvasY) {
if (canvasY >= widgetTop && canvasY <= widgetBottom) {
// Found the widget at this position
if (widget.name?.startsWith("lora_")) {
// Check if click is on the low variant icon (green dot)
// Check if click is on the low variant icon (circle)
if (widget.hitAreas && widget.hitAreas.low_variant_icon) {
const iconBounds = widget.hitAreas.low_variant_icon.bounds;
// iconBounds is [posX, width, height?]
const iconAbsXStart = this.pos[0] + iconBounds[0];
const iconAbsXEnd = iconAbsXStart + (iconBounds[1] || 20);
const iconAbsYStart = widgetTop;
@@ -238,354 +209,11 @@ export function getLoraSlotInPosition(canvasX, canvasY) {
}
export function getLoraSlotMenuOptions(slot, event) {
// Check if the slot is specifically for the LOW_VARIANT_ICON
if (slot?.output?.type === "LOW_VARIANT_ICON") {
const canvas = app.canvas;
// Try multiple ways to get the screen position
let x = 100;
let y = 100;
// Option 1: Try to get from last_mouse_position (canvas screen coords)
if (canvas.last_mouse_position) {
x = canvas.last_mouse_position[0];
y = canvas.last_mouse_position[1];
}
// Option 2: Try canvas_mouse (might be available)
else if (canvas.canvas_mouse) {
x = canvas.canvas_mouse[0];
y = canvas.canvas_mouse[1];
}
// Option 3: Convert graph coordinates to screen coordinates
else if (canvas.graph_mouse && canvas.ds) {
const graphX = canvas.graph_mouse[0];
const graphY = canvas.graph_mouse[1];
// Convert from graph space to canvas space
x = graphX * canvas.ds.scale + canvas.ds.offset[0];
y = graphY * canvas.ds.scale + canvas.ds.offset[1];
// Convert from canvas space to screen space
const canvasRect = canvas.canvas.getBoundingClientRect();
x += canvasRect.left;
y += canvasRect.top;
}
console.log("Low variant menu position calculated:", {x, y});
// Use a slight delay to ensure the menu shows after the event completes
setTimeout(() => {
showLowVariantMenu(event, slot.widget, this, { x, y });
}, 10);
// Return false to prevent LiteGraph from showing its default menu
return false;
const items = buildSlotMenuItems(slot, this);
if (items) {
// Must return a real options array: newer ComfyUI frontends feed the
// return value straight into new LiteGraph.ContextMenu(options, ...).
return items;
}
// Check if the slot is for a LoRA widget (layer right-click)
if (slot?.output?.type === "LORA_WIDGET" || slot?.widget?.name?.startsWith("lora_")) {
const canvas = app.canvas;
// Try multiple ways to get the screen position
let x = 100;
let y = 100;
// Option 1: Try to get from last_mouse_position (canvas screen coords)
if (canvas.last_mouse_position) {
x = canvas.last_mouse_position[0];
y = canvas.last_mouse_position[1];
}
// Option 2: Try canvas_mouse (might be available)
else if (canvas.canvas_mouse) {
x = canvas.canvas_mouse[0];
y = canvas.canvas_mouse[1];
}
// Option 3: Convert graph coordinates to screen coordinates
else if (canvas.graph_mouse && canvas.ds) {
const graphX = canvas.graph_mouse[0];
const graphY = canvas.graph_mouse[1];
// Convert from graph space to canvas space
x = graphX * canvas.ds.scale + canvas.ds.offset[0];
y = graphY * canvas.ds.scale + canvas.ds.offset[1];
// Convert from canvas space to screen space
const canvasRect = canvas.canvas.getBoundingClientRect();
x += canvasRect.left;
y += canvasRect.top;
}
console.log("LoRA menu position calculated:", {x, y});
// Use a slight delay to ensure the menu shows after the event completes
setTimeout(() => {
showCustomLoraMenu(event, slot.widget, this, { x, y });
}, 10);
// Return false to prevent LiteGraph from showing its default menu
return false;
}
return LGraphNode.prototype.getSlotMenuOptions?.call(this, slot);
}
// Function to display a simple context menu for low variant information
function showLowVariantMenu(event, widget, node, position) {
console.log("showLowVariantMenu called.");
// Remove any existing menu to prevent duplicates
const existingMenu = document.getElementById('low-variant-menu');
if (existingMenu) {
existingMenu.remove();
}
// Determine which state this widget is in
const hasLowVariant = widget.value.is_low && widget.value.low_variant_name;
const isBothMode = widget.value.use_for_both;
const isLowOnlyMode = widget.value.low_only;
const isHighOnlyMode = widget.value.high_only;
const menu = document.createElement('div');
menu.id = 'low-variant-menu';
menu.className = 'litegraph litecontextmenu litemenubar-panel';
menu.style.cssText = `
position: absolute !important;
left: ${position.x}px !important;
top: ${position.y + 60}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: 200px !important;
display: block !important;
font-family: Arial, sans-serif !important;
font-size: 11px !important;
`;
// Helper to create menu item
const createMenuItem = (icon, 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);
document.removeEventListener('contextmenu', hideMenu);
document.removeEventListener('mousedown', hideMenu);
};
}
const iconSpan = document.createElement('span');
iconSpan.textContent = icon;
iconSpan.style.cssText = `
font-size: 12px !important;
width: 16px !important;
text-align: center !important;
`;
const textSpan = document.createElement('span');
textSpan.textContent = text;
item.appendChild(iconSpan);
item.appendChild(textSpan);
return item;
};
// Helper to create separator
const createSeparator = () => {
const separator = document.createElement('div');
separator.style.cssText = `
height: 1px !important;
background-color: #333 !important;
margin: 2px 0 !important;
`;
return separator;
};
if (hasLowVariant || isBothMode || isLowOnlyMode || isHighOnlyMode) {
// For low variant, both mode, low only mode, or high only mode: show the name
let displayText;
if (isBothMode) {
displayText = `${widget.value.lora} (both)`;
} else if (isLowOnlyMode) {
displayText = `${widget.value.lora} (low only)`;
} else if (isHighOnlyMode) {
displayText = `${widget.value.lora} (high only)`;
} else {
displayText = widget.value.low_variant_name;
}
const fileName = displayText.includes('/') || displayText.includes('\\')
? displayText.split(/[\/\\]/).pop()
: displayText;
const nameItem = document.createElement('div');
nameItem.style.cssText = `
padding: 4px 8px !important;
cursor: default !important;
color: #ddd !important;
background-color: #1a1a1a !important;
user-select: none !important;
display: flex !important;
align-items: center !important;
`;
const textSpan = document.createElement('span');
textSpan.textContent = fileName;
nameItem.appendChild(textSpan);
menu.appendChild(nameItem);
// Add separator
menu.appendChild(createSeparator());
if (hasLowVariant) {
// For low variant: 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', () => {
widget.showLoraInfoDialog('low');
}));
} else if (isBothMode) {
// For both mode: add Disable and switching options
menu.appendChild(createMenuItem('⚪', 'Disable', () => {
widget.value.use_for_both = false;
widget.value.low_only = false;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🔵', 'Switch to Low only', () => {
widget.value.use_for_both = false;
widget.value.low_only = true;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🔴', 'Switch to High only', () => {
widget.value.use_for_both = false;
widget.value.low_only = false;
widget.value.high_only = true;
node.setDirtyCanvas(true, true);
}));
} else if (isLowOnlyMode) {
// For low only mode: add Disable and switching options
menu.appendChild(createMenuItem('⚪', 'Disable', () => {
widget.value.use_for_both = false;
widget.value.low_only = false;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🟣', 'Switch to Both', () => {
widget.value.use_for_both = true;
widget.value.low_only = false;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🔴', 'Switch to High only', () => {
widget.value.use_for_both = false;
widget.value.low_only = false;
widget.value.high_only = true;
node.setDirtyCanvas(true, true);
}));
} else if (isHighOnlyMode) {
// For high only mode: add Disable and switching options
menu.appendChild(createMenuItem('⚪', 'Disable', () => {
widget.value.use_for_both = false;
widget.value.low_only = false;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🟣', 'Switch to Both', () => {
widget.value.use_for_both = true;
widget.value.low_only = false;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🔵', 'Switch to Low only', () => {
widget.value.use_for_both = false;
widget.value.low_only = true;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
}
} else {
// No low variant: show all three options
menu.appendChild(createMenuItem('🟣', 'Enable for both', () => {
widget.value.use_for_both = true;
widget.value.low_only = false;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🔵', 'Low only', () => {
widget.value.use_for_both = false;
widget.value.low_only = true;
widget.value.high_only = false;
node.setDirtyCanvas(true, true);
}));
menu.appendChild(createMenuItem('🔴', 'High only', () => {
widget.value.use_for_both = false;
widget.value.low_only = false;
widget.value.high_only = true;
node.setDirtyCanvas(true, true);
}));
}
document.body.appendChild(menu);
console.log("Low variant menu appended to body, position:", position);
// Prevent menu close when interacting with menu
menu.onclick = (e) => e.stopPropagation();
menu.onmousedown = (e) => e.stopPropagation();
// Hide menu when clicking outside
const hideMenu = (e) => {
if (menu && !menu.contains(e.target)) {
console.log("Hiding low variant menu");
menu.remove();
document.removeEventListener('click', hideMenu);
document.removeEventListener('contextmenu', hideMenu);
document.removeEventListener('mousedown', hideMenu);
}
};
// Add listeners after a short delay to prevent immediate closure
setTimeout(() => {
document.addEventListener('click', hideMenu);
document.addEventListener('contextmenu', hideMenu);
document.addEventListener('mousedown', hideMenu);
}, 100);
}
+212 -30
View File
@@ -7,7 +7,6 @@ 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
// Remove any existing context menu
const existingMenu = document.getElementById('lora-list-context-menu');
if (existingMenu) {
@@ -91,7 +90,6 @@ function showLoraListContextMenu(event, 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');
}
@@ -128,7 +126,6 @@ function showLoraListContextMenu(event, loraName) {
.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');
}
@@ -187,11 +184,9 @@ function showLoraInfoDialog(loraName) {
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
console.log("LoRA info was modified");
}
}));
}).catch(error => {
@@ -211,17 +206,14 @@ function showModelInfoDialog(modelName) {
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);
@@ -237,11 +229,8 @@ function showModelInfoDialog(modelName) {
// 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);
@@ -257,10 +246,8 @@ function showModelInfoDialog(modelName) {
// 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);
@@ -273,7 +260,6 @@ function showModelInfoDialog(modelName) {
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
@@ -377,6 +363,65 @@ style.textContent = `
flex: 1 !important;
}
.lora-picker-folder-arrow {
font-size: 9px !important;
color: #999 !important;
margin-left: auto !important;
margin-right: 2px !important;
padding: 1px 4px !important;
cursor: pointer !important;
user-select: none !important;
flex-shrink: 0 !important;
border-radius: 2px !important;
transition: color 0.2s !important;
}
.lora-picker-folder-arrow:hover {
color: #fff !important;
background-color: #3a3a3a !important;
}
.lora-picker-subfolder-item {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
padding: 2px 4px 2px 18px !important;
cursor: pointer !important;
border-radius: 0px !important;
transition: background-color 0.2s !important;
margin: 0 !important;
border-left: 2px solid transparent !important;
border-right: 2px solid transparent !important;
}
.lora-picker-subfolder-item:hover {
background-color: #333 !important;
}
.lora-picker-subfolder-item.selected {
background-color: #2a2a2a !important;
border-left: 2px solid #89B !important;
border-right: 2px solid #89B !important;
}
.lora-picker-subfolder-icon {
font-size: 10px !important;
margin-right: 4px !important;
color: #89B !important;
flex-shrink: 0 !important;
}
.lora-picker-subfolder-name {
font-size: 10px !important;
text-align: left !important;
color: #bbb !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
user-select: none !important;
flex: 1 !important;
}
.lora-picker-main-content {
flex: 1 !important;
display: flex !important;
@@ -664,6 +709,16 @@ function loadSelectedFolder() {
return selectedFolder !== null ? JSON.parse(selectedFolder) : null;
}
// Persistence functions for selected subfolder
function saveSelectedSubfolder(selectedSubfolder) {
localStorage.setItem("wanVideoPowerLoraLoader.selectedSubfolder", JSON.stringify(selectedSubfolder));
}
function loadSelectedSubfolder() {
const selectedSubfolder = localStorage.getItem("wanVideoPowerLoraLoader.selectedSubfolder");
return selectedSubfolder !== null ? JSON.parse(selectedSubfolder) : null;
}
// Persistence functions for eye refresh button state
function saveEyeRefreshState(eyeRefreshState) {
localStorage.setItem("wanVideoPowerLoraLoader.eyeRefreshState", JSON.stringify(eyeRefreshState));
@@ -679,12 +734,14 @@ function saveAllUIState(dialog) {
foldersVisible: dialog.options.foldersVisible,
eyeRefreshState: dialog.eyeRefreshState,
selectedFolder: dialog.selectedFolder,
selectedSubfolder: dialog.selectedSubfolder || null,
circleIconState: dialog.circleIconState
};
saveFoldersVisible(dialog.options.foldersVisible);
saveEyeRefreshState(dialog.eyeRefreshState);
saveSelectedFolder(dialog.selectedFolder);
saveSelectedSubfolder(dialog.selectedSubfolder || null);
if (dialog.parentNode && dialog.parentNode.widgets) {
// Try both widget names to support both node types
@@ -707,6 +764,7 @@ function loadAllUIState(dialog) {
foldersVisible: undefined,
eyeRefreshState: undefined,
selectedFolder: undefined,
selectedSubfolder: undefined,
circleIconState: undefined
};
@@ -734,6 +792,9 @@ function loadAllUIState(dialog) {
if (uiState.selectedFolder === undefined) {
uiState.selectedFolder = loadSelectedFolder();
}
if (uiState.selectedSubfolder === undefined) {
uiState.selectedSubfolder = loadSelectedSubfolder();
}
if (uiState.circleIconState === undefined) {
uiState.circleIconState = 0; // Default to 0 (empty circle)
}
@@ -741,6 +802,7 @@ function loadAllUIState(dialog) {
dialog.options.foldersVisible = uiState.foldersVisible !== false;
dialog.eyeRefreshState = uiState.eyeRefreshState || false;
dialog.selectedFolder = uiState.selectedFolder || null;
dialog.selectedSubfolder = uiState.selectedSubfolder || null;
dialog.circleIconState = uiState.circleIconState || 0;
if (dialog.parentNode && dialog.parentNode.widgets) {
@@ -771,6 +833,10 @@ export class LoraPickerDialog {
this.sidebarWidth = 150;
// Subfolder support state (must exist before loadAllUIState restores it)
this.expandedFolders = new Set();
this.selectedSubfolder = null;
loadAllUIState(this);
// Initialize circle icon state if not already set
@@ -1343,38 +1409,132 @@ export class LoraPickerDialog {
}
}
// Returns the distinct subfolder names (second path segment) under a given
// top-level folder - only subfolders that directly contain loras.
getSubfolders(parentFolder) {
const subs = new Set();
for (const lora of this.loras) {
const name = typeof lora === 'string' ? lora : lora.name;
if (!name) continue;
const parts = name.split(/[\/\\]/);
if (parts.length >= 3 && parts[0] === parentFolder && parts[1]) {
subs.add(parts[1]);
}
}
return [...subs].sort((a, b) => a.localeCompare(b));
}
renderFoldersInSidebar(container) {
container.innerHTML = "";
for (const folder of this.folders) {
// Wrapper holds the folder row and (when expanded) its subfolders
const wrapper = document.createElement("div");
wrapper.className = "lora-picker-folder-wrapper";
const folderItem = document.createElement("div");
folderItem.className = "lora-picker-folder-item";
if (this.selectedFolder === folder) {
folderItem.classList.add("selected");
}
// Folder icon
const folderIcon = document.createElement("div");
folderIcon.className = "lora-picker-folder-icon";
folderIcon.textContent = "📁";
// Folder name
const folderName = document.createElement("div");
folderName.className = "lora-picker-folder-name";
folderName.textContent = folder;
folderItem.appendChild(folderIcon);
folderItem.appendChild(folderName);
// Add click handler
// Add click handler for the folder row
folderItem.addEventListener("click", () => {
this.selectFolder(folder);
});
container.appendChild(folderItem);
// Folder row goes first...
wrapper.appendChild(folderItem);
// Down arrow for folders that have subdirectories (right-aligned).
// Only shows subfolders 1 level deep.
const subfolders = folder === "root" ? [] : this.getSubfolders(folder);
if (subfolders.length > 0) {
const isExpanded = this.expandedFolders.has(folder);
const arrow = document.createElement("div");
arrow.className = "lora-picker-folder-arrow";
arrow.textContent = isExpanded ? "▼" : "▶";
arrow.title = isExpanded ? "Collapse subfolders" : "Show subfolders";
arrow.addEventListener("click", (e) => {
e.stopPropagation();
if (this.expandedFolders.has(folder)) {
this.expandedFolders.delete(folder);
} else {
this.expandedFolders.add(folder);
}
this.renderFoldersInSidebar(container);
});
folderItem.appendChild(arrow);
// ...then the subfolder rows underneath it
if (isExpanded) {
for (const sub of subfolders) {
const subItem = document.createElement("div");
subItem.className = "lora-picker-subfolder-item";
if (this.selectedFolder === folder && this.selectedSubfolder === sub) {
subItem.classList.add("selected");
}
const subIcon = document.createElement("div");
subIcon.className = "lora-picker-subfolder-icon";
subIcon.textContent = "📂";
const subName = document.createElement("div");
subName.className = "lora-picker-subfolder-name";
subName.textContent = sub;
subItem.appendChild(subIcon);
subItem.appendChild(subName);
subItem.addEventListener("click", () => {
this.selectSubfolder(folder, sub);
});
wrapper.appendChild(subItem);
}
}
}
container.appendChild(wrapper);
}
}
getSidebarFolderContainer() {
return this.sidebar
? (this.sidebar.querySelector(".lora-picker-sidebar-content") || this.sidebar)
: null;
}
selectSubfolder(parentFolder, subfolder) {
// Clicking the already selected subfolder deselects it (parent stays)
if (this.selectedFolder === parentFolder && this.selectedSubfolder === subfolder) {
this.selectedSubfolder = null;
saveAllUIState(this);
this.renderFoldersInSidebar(this.getSidebarFolderContainer());
this.renderList();
return;
}
this.selectedFolder = parentFolder;
this.selectedSubfolder = subfolder;
saveAllUIState(this);
this.renderFoldersInSidebar(this.getSidebarFolderContainer());
this.renderList();
}
extractFolders() {
const folderSet = new Set();
// Always add "root" folder
@@ -1410,20 +1570,25 @@ export class LoraPickerDialog {
prevSelected.classList.remove("selected");
}
this.selectedFolder = null;
this.selectedSubfolder = null;
// Save all UI state to both localStorage and workflow
saveAllUIState(this);
// Update indicator line visibility
this.updateFolderIndicator();
this.renderFoldersInSidebar(this.getSidebarFolderContainer());
this.renderList(); // Refresh the list
return;
}
// Remove selection from previous folder
const prevSelected = this.sidebar.querySelector(".lora-picker-folder-item.selected");
if (prevSelected) {
prevSelected.classList.remove("selected");
}
// Switching to a different folder clears any subfolder filter
this.selectedSubfolder = null;
// Add selection to new folder
const folderItems = this.sidebar.querySelectorAll(".lora-picker-folder-item");
for (const item of folderItems) {
@@ -1433,12 +1598,13 @@ export class LoraPickerDialog {
break;
}
}
this.selectedFolder = folderName;
// Save all UI state to both localStorage and workflow
saveAllUIState(this);
// Update indicator line visibility
this.updateFolderIndicator();
this.renderFoldersInSidebar(this.getSidebarFolderContainer());
this.renderList(); // Refresh the list
}
@@ -1455,12 +1621,23 @@ export class LoraPickerDialog {
// Remove folder prefix for display purposes only (first level only)
let displayName = name;
if (this.selectedFolder && this.selectedFolder !== "root") {
const prefix = this.selectedFolder + "/";
const altPrefix = this.selectedFolder + "\\";
// Strip the subfolder prefix too when one is selected
const prefix = this.selectedSubfolder
? this.selectedFolder + "/" + this.selectedSubfolder + "/"
: this.selectedFolder + "/";
const altPrefix = prefix.replace("/", "\\");
if (name.startsWith(prefix)) {
displayName = name.substring(prefix.length);
} else if (name.startsWith(altPrefix)) {
displayName = name.substring(altPrefix.length);
} else if (!this.selectedSubfolder) {
const plainPrefix = this.selectedFolder + "/";
const plainAltPrefix = this.selectedFolder + "\\";
if (name.startsWith(plainPrefix)) {
displayName = name.substring(plainPrefix.length);
} else if (name.startsWith(plainAltPrefix)) {
displayName = name.substring(plainAltPrefix.length);
}
}
}
@@ -1582,8 +1759,15 @@ export class LoraPickerDialog {
let filteredLoras = loras;
// Filter by selected folder
if (this.selectedFolder && this.selectedFolder !== "root") {
// Filter by selected folder + subfolder (subfolder takes precedence)
if (this.selectedFolder && this.selectedFolder !== "root" && this.selectedSubfolder) {
const prefixFwd = this.selectedFolder + "/" + this.selectedSubfolder + "/";
const prefixBck = this.selectedFolder + "\\" + this.selectedSubfolder + "\\";
filteredLoras = filteredLoras.filter(l => {
const name = typeof l === 'string' ? l : l.name;
return !!name && (name.startsWith(prefixFwd) || name.startsWith(prefixBck));
});
} else if (this.selectedFolder && this.selectedFolder !== "root") {
filteredLoras = filteredLoras.filter(l => {
const name = typeof l === 'string' ? l : l.name;
// Check if the lora is in the selected folder
@@ -2104,7 +2288,6 @@ export class LoraPickerDialog {
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')
@@ -2262,7 +2445,6 @@ export class LoraPickerDialog {
// 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);
}
@@ -1,4 +1,4 @@
import { rgthreeApi } from "./rgthree_api.js";
import { vidApi } from "./wanvid_api.js";
import { api } from "../../../scripts/api.js";
import { generatePreviewFromFirstImage } from "./image_utils.js";
@@ -22,15 +22,15 @@ class BaseModelInfoService extends EventTarget {
}
async clearFetchedInfo(file) {
if (this.modelInfoType === 'loras') {
await rgthreeApi.clearSingleLoraInfo(file);
await vidApi.clearSingleLoraInfo(file);
} else if (this.modelInfoType === 'checkpoints') {
await rgthreeApi.clearSingleCheckpointInfo(file);
await vidApi.clearSingleCheckpointInfo(file);
}
this.fileToInfo.delete(file);
return null;
}
async savePartialInfo(file, data, options = {}) {
let info = await rgthreeApi.saveModelInfo(this.modelInfoType, file, data);
let info = await vidApi.saveModelInfo(this.modelInfoType, file, data);
this.fileToInfo.set(file, info);
// Generate preview image for LoRAs ONLY if explicitly requested and we have image data
@@ -44,7 +44,6 @@ class BaseModelInfoService extends EventTarget {
// Generate and save preview image
const previewResult = await generatePreviewFromFirstImage(info, loraName, loraPath);
console.log(`[LoraInfoService] Preview image generated for ${loraName}:`, previewResult);
} catch (error) {
console.warn(`[LoraInfoService] Failed to generate preview image for ${file}:`, error);
// Don't let preview generation failure stop the save operation
@@ -64,10 +63,10 @@ class BaseModelInfoService extends EventTarget {
var _a;
let info = null;
if (!refresh) {
info = await rgthreeApi.getModelsInfo({ type: this.modelInfoType, files: [file], light });
info = await vidApi.getModelsInfo({ type: this.modelInfoType, files: [file], light });
}
else {
info = await rgthreeApi.refreshModelsInfo({ type: this.modelInfoType, files: [file] });
info = await vidApi.refreshModelsInfo({ type: this.modelInfoType, files: [file] });
}
info = (_a = info === null || info === void 0 ? void 0 : info[0]) !== null && _a !== void 0 ? _a : null;
if (!light) {
+5 -16
View File
@@ -1,7 +1,7 @@
import { app } from "../../../scripts/app.js";
import { rgthree } from "../rgthree/common/rgthree.js";
import { RgthreeBetterButtonWidget, RgthreeDividerWidget, CombinedOptionsWidget, PowerLoraLoaderHeaderWidget, PowerLoraLoaderWidget } from "./widgets.js";
import { qq } from "../qq_lib/common/qq.js";
import { QQBetterButtonWidget, QQDividerWidget, CombinedOptionsWidget, PowerLoraLoaderHeaderWidget, PowerLoraLoaderWidget } from "./widgets.js";
import { LoraPickerDialog } from "./lora_picker_dialog.js";
import { getLoraSlotInPosition, getLoraSlotMenuOptions } from "./lora_context_menu.js";
import { modelDataManager, UIManager } from "./pull_info.js";
@@ -51,19 +51,15 @@ function showLoraPicker(event, callback, node = null) {
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);
}
@@ -159,10 +155,10 @@ app.registerExtension({
nodeType.prototype.addNonLoraWidgets = function () {
this.addCustomWidget(new CombinedOptionsWidget());
this.addCustomWidget(new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new QQDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new PowerLoraLoaderHeaderWidget());
this.widgetButtonSpacer = this.addCustomWidget(new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new RgthreeBetterButtonWidget("➕ Add Lora", (event, pos, node) => {
this.widgetButtonSpacer = this.addCustomWidget(new QQDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new QQBetterButtonWidget("➕ Add Lora", (event, pos, node) => {
showLoraPicker(event, (lora) => {
if (lora) {
this.addNewLoraWidget().setLora(lora);
@@ -328,12 +324,6 @@ app.registerExtension({
// Re-run the low variant check with updated cache
widget.checkLowLoraVariant(widget.value.lora);
// Log changes in low variant detection
if (oldIsLow !== widget.value.is_low || oldVariantName !== widget.value.low_variant_name) {
console.log(`[WanVideoPowerLoraLoader] Low variant status changed for '${widget.value.lora}': ` +
`${oldIsLow} -> ${widget.value.is_low}, ` +
`variant: '${oldVariantName}' -> '${widget.value.low_variant_name}'`);
}
}
// Trigger a value change to update the UI
@@ -348,7 +338,6 @@ app.registerExtension({
}
});
console.log('[WanVideoPowerLoraLoader] LoRA definitions refreshed - individual LoRA info available in dialogs');
}).catch(error => {
console.error('[WanVideoPowerLoraLoader] Error refreshing LoRA list:', error);
});
@@ -19,9 +19,10 @@
import { app } from "../../../scripts/app.js";
import {
RgthreeBaseWidget, RgthreeBetterButtonWidget, RgthreeDividerWidget,
QQBaseWidget, QQBetterButtonWidget, QQDividerWidget,
StrengthCopyWidget, PowerLoraLoaderHeaderWidget, PowerLoraLoaderWidget,
fitString, drawTogglePart, drawRoundedRectangle,
drawNumberWidgetPart, drawInfoIcon,
} from "./widgets.js";
import { LoraPickerDialog } from "./lora_picker_dialog.js";
import { getLoraSlotInPosition, getLoraSlotMenuOptions } from "./lora_context_menu.js";
@@ -183,12 +184,166 @@ export class PowerLoraLoaderWidgetEx extends PowerLoraLoaderWidget {
if (!strengthSplit) {
// Single-strength mode: no Low strength hit area / interactions
delete this.hitAreas.low_strength;
// Simple mode: no colored variant circle at the end of the row,
// so also remove its hit area (no variant menu on right-click there)
delete this.hitAreas.low_variant_icon;
// Simple mode extras (borrowed from qq's Power Lora Loader):
// "i" info badge next to the strength, and "<" / ">" steppers.
this.hitAreas.info = { bounds: [0, 0], onDown: this.onInfoDown };
this.hitAreas.strengthDec = { bounds: [0, 0], onDown: this.onStrengthDecDown };
this.hitAreas.strengthInc = { bounds: [0, 0], onDown: this.onStrengthIncDown };
// Drag-to-change area spanning the whole "< value >" part; the
// click-to-prompt popup only fires on the number itself (strength).
this.hitAreas.strengthAny = { bounds: [0, 0], onMove: this.onStrengthAnyMove };
}
}
// --- Simple-mode only handlers (qq-style) ---
onInfoDown(event, pos, node) {
if (!this.value.lora || this.value.lora === "None") return;
this.cancelMouseDown();
this.showLoraInfoDialog('high');
}
onStrengthDecDown(event, pos, node) {
this.stepStrength(-1);
node.setDirtyCanvas(true, false);
}
onStrengthIncDown(event, pos, node) {
this.stepStrength(1);
node.setDirtyCanvas(true, false);
}
stepStrength(direction) {
const step = 0.05;
const strength = (this.value.strength ?? 1) + step * direction;
this.value.strength = Math.round(strength * 100) / 100;
}
// --- Drag-to-reorder (Shift + Left Click on the LoRA name) ---
// Mechanism ported from Power Spline Editor's layer widgets.
mouse(event, pos, node) {
// While a reorder drag is active, swallow move/up events so no hit
// areas (toggle, strength drag, lora chooser...) fire accidentally.
if (this._reorderDragActive) {
if (event.type === 'pointerup' || event.type === 'pointermove') {
return true;
}
}
// Shift + Left Click on the lora name => begin drag-to-reorder
if (event.type === 'pointerdown' && event.shiftKey && event.button === 0) {
if (this.clickWasWithinBounds(pos, this.hitAreas.lora.bounds)) {
this._beginLoraReorderDrag(event, node);
return true;
}
}
return super.mouse(event, pos, node);
}
_beginLoraReorderDrag(pointerDownEvent, node) {
const getLoras = () => node.widgets.filter(w => w.name?.startsWith("lora_"));
const loras = getLoras();
const startIndex = loras.indexOf(this);
if (startIndex < 0) return;
this._reorderDragActive = true;
this._reorderStartIndex = startIndex;
this._reorderTargetIndex = startIndex;
this._reorderStartClientY = pointerDownEvent.clientY;
// Cursor feedback
this._prevBodyCursor = document.body.style.cursor;
document.body.style.cursor = 'grabbing';
try { app.canvas.canvas.style.cursor = 'grabbing'; } catch {}
const onMove = (e) => {
if (!this._reorderDragActive) return;
const dy = (e.clientY - this._reorderStartClientY) || 0;
const step = Math.round(dy / (LiteGraph.NODE_WIDGET_HEIGHT || 24));
let idx = this._reorderStartIndex + step;
// Allow dropping after the last lora (idx === loras.length)
idx = Math.max(0, Math.min(loras.length, idx));
if (idx !== this._reorderTargetIndex) {
this._reorderTargetIndex = idx;
}
try { node.setDirtyCanvas(true, true); } catch {}
};
const onUp = () => {
document.removeEventListener('pointermove', onMove, true);
document.removeEventListener('pointerup', onUp, true);
// Restore cursor
try { app.canvas.canvas.style.cursor = ''; } catch {}
document.body.style.cursor = this._prevBodyCursor || '';
if (!this._reorderDragActive) return;
this._reorderDragActive = false;
const lorasNow = getLoras();
const from = lorasNow.indexOf(this);
const to = this._reorderTargetIndex ?? from;
if (from >= 0 && to >= 0 && from !== to) {
const widgets = node.widgets;
// Remove from current global position
widgets.splice(widgets.indexOf(this), 1);
if (to < lorasNow.length) {
// Insert before the target lora widget
const before = lorasNow[to];
const targetGlobal = widgets.indexOf(before);
widgets.splice(targetGlobal >= 0 ? targetGlobal : widgets.length, 0, this);
} else {
// Insert after the last lora widget
const afterRemoval = getLoras();
const last = afterRemoval[afterRemoval.length - 1];
const lastIdx = last ? widgets.indexOf(last) : -1;
widgets.splice(lastIdx >= 0 ? lastIdx + 1 : widgets.length, 0, this);
}
try { node.setDirtyCanvas(true, true); } catch {}
}
this._reorderStartIndex = undefined;
this._reorderTargetIndex = undefined;
this._reorderStartClientY = undefined;
};
document.addEventListener('pointermove', onMove, true);
document.addEventListener('pointerup', onUp, true);
}
// Cyan insertion indicator line, drawn at the widget sitting at the
// current drop target index while a reorder drag is active.
_drawReorderIndicatorIfTarget(ctx, node, y, h) {
try {
const loras = node.widgets.filter(w => w.name?.startsWith("lora_"));
const dragging = loras.find(w => w && w._reorderDragActive);
if (!dragging) return;
const target = dragging._reorderTargetIndex ?? -1;
const myIndex = loras.indexOf(this);
const isLast = myIndex === loras.length - 1;
const showTopLine = myIndex === target && target < loras.length;
const showBottomLine = isLast && target === loras.length; // after-last sentinel
if (showTopLine || showBottomLine) {
ctx.save();
ctx.strokeStyle = '#2cc6ff';
ctx.globalAlpha = app.canvas.editor_alpha;
ctx.lineWidth = 2;
ctx.beginPath();
const left = 10;
const right = node.size[0] - 10;
const lineY = showTopLine ? (y - 1) : (y + h - 1);
ctx.moveTo(left, lineY);
ctx.lineTo(right, lineY);
ctx.stroke();
ctx.restore();
}
} catch {}
}
draw(ctx, node, w, y, h) {
if (this.strengthSplit) {
super.draw(ctx, node, w, y, h);
this._drawReorderIndicatorIfTarget(ctx, node, y, h);
return;
}
@@ -202,38 +357,37 @@ export class PowerLoraLoaderWidgetEx extends PowerLoraLoaderWidget {
this.hitAreas.toggle.bounds = drawTogglePart(ctx, [posX, y], h, this.value.on);
posX += this.hitAreas.toggle.bounds[1] + MARGIN;
let rposX = w - MARGIN;
if (this.value.use_for_both) {
ctx.fillStyle = "#9370DB"; // Purple for "both" state
} else if (this.value.low_only) {
ctx.fillStyle = "#4169E1"; // Blue for "low only" state
} else if (this.value.high_only) {
ctx.fillStyle = "#DC143C"; // Red for "high only" state
} else 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();
// Simple mode: no colored circle at the end of the row. Instead, from
// the right edge: "< strength >" stepper, then the "i" info badge
// (v2 keeps the circle + plain Low/High text values).
const innerMargin = MARGIN * 0.33;
const iconGap = 5; // extra breathing room
// right offset => spacing after the ">" arrow
let rposX = w - MARGIN - innerMargin - iconGap;
// Store the bounds for the low variant icon
this.hitAreas.low_variant_icon.bounds = [
rposX - ICON_WIDTH,
y,
ICON_WIDTH,
h
];
rposX -= ICON_WIDTH + MARGIN;
this.hitAreas.strength.bounds = [rposX - STRENGTH_WIDTH, STRENGTH_WIDTH];
ctx.textAlign = "right";
// "< strength >" arrows. Clicking the number opens the prompt popup;
// the arrows only step the value (no popup). Dragging anywhere over
// the part changes the value.
ctx.fillStyle = "white";
ctx.textBaseline = "middle";
ctx.fillText(this.value.strength.toFixed(2), rposX, y + h / 2);
rposX -= STRENGTH_WIDTH + MARGIN;
const [leftArrow, textBounds, rightArrow] = drawNumberWidgetPart(ctx, {
posX: rposX,
posY: y,
height: h,
value: this.value.strength ?? 1,
direction: -1,
});
this.hitAreas.strengthDec.bounds = leftArrow;
this.hitAreas.strengthInc.bounds = rightArrow;
this.hitAreas.strength.bounds = textBounds; // click => prompt popup
this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; // drag
// spacing after the "i" icon (gap between icon and stepper)
rposX = leftArrow[0] - innerMargin - iconGap;
// "i" info badge -> Show Info dialog (gray, not blue-outlined)
const infoIconSize = h * 0.66;
drawInfoIcon(ctx, rposX - infoIconSize, y + (h - infoIconSize) / 2, infoIconSize, "GRAYED");
this.hitAreas.info.bounds = [rposX - infoIconSize - innerMargin, infoIconSize + innerMargin * 2];
rposX -= infoIconSize + innerMargin * 2 + innerMargin;
const loraWidth = rposX - posX;
this.hitAreas.lora.bounds = [posX, loraWidth];
@@ -241,6 +395,7 @@ export class PowerLoraLoaderWidgetEx extends PowerLoraLoaderWidget {
ctx.fillText(fitString(ctx, this.value.lora, loraWidth), posX, y + h / 2);
ctx.globalAlpha = 1;
this._drawReorderIndicatorIfTarget(ctx, node, y, h);
}
}
@@ -393,10 +548,10 @@ export function createPowerLoraLoaderExtension(config) {
if (outputBoth || copyButtons) {
this.addCustomWidget(new StrengthCopyWidget(outputBoth, copyButtons));
}
this.addCustomWidget(new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new QQDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new PowerLoraLoaderHeaderWidgetEx(strengthSplit));
this.widgetButtonSpacer = this.addCustomWidget(new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new RgthreeBetterButtonWidget("➕ Add Lora", (event, pos, node) => {
this.widgetButtonSpacer = this.addCustomWidget(new QQDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new QQBetterButtonWidget("➕ Add Lora", (event, pos, node) => {
showPicker(event, (lora) => {
if (lora) {
this.addNewLoraWidget().setLora(lora);
@@ -592,12 +747,6 @@ export function createPowerLoraLoaderExtension(config) {
// Re-run the low variant check with updated cache
widget.checkLowLoraVariant(widget.value.lora);
// Log changes in low variant detection
if (oldIsLow !== widget.value.is_low || oldVariantName !== widget.value.low_variant_name) {
console.log(`[${logTag}] Low variant status changed for '${widget.value.lora}': ` +
`${oldIsLow} -> ${widget.value.is_low}, ` +
`variant: '${oldVariantName}' -> '${widget.value.low_variant_name}'`);
}
}
// Trigger a value change to update the UI
@@ -612,7 +761,6 @@ export function createPowerLoraLoaderExtension(config) {
}
});
console.log(`[${logTag}] LoRA definitions refreshed - individual LoRA info available in dialogs`);
}).catch(error => {
console.error(`[${logTag}] Error refreshing LoRA list:`, error);
});
@@ -1,6 +1,6 @@
import { app } from "../../../scripts/app.js";
import { rgthree } from "../rgthree/common/rgthree.js";
import { RgthreeBetterButtonWidget, RgthreeDividerWidget, RgthreeBaseWidget } from "./widgets.js";
import { qq } from "../qq_lib/common/qq.js";
import { QQBetterButtonWidget, QQDividerWidget, QQBaseWidget } 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";
@@ -52,7 +52,7 @@ function loadEyeRefreshState() {
// Custom widget for model selection
class PowerModelLoaderWidget extends RgthreeBaseWidget {
class PowerModelLoaderWidget extends QQBaseWidget {
constructor(name, showModelChooser) {
super(name);
this.showModelChooser = showModelChooser;
@@ -420,7 +420,7 @@ app.registerExtension({
};
nodeType.prototype.addNonModelWidgets = function () {
this.addCustomWidget(new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
this.addCustomWidget(new QQDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }));
// Add hidden widget for UI state persistence
const uiStateWidget = this.addWidget("string", "ui_state", "{}", () => {},
+5 -7
View File
@@ -3,7 +3,7 @@
* 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";
import { vidApi } from "./wanvid_api.js";
// ============================================================================
// DIRECTORY STRUCTURE MANAGER
@@ -183,14 +183,14 @@ class ModelDataManager {
try {
let itemsData;
// Use appropriate rgthreeApi method
// Use appropriate vidApi method
if (type === 'checkpoints') {
itemsData = await wanvidApi.getCheckpoints(forceRefresh);
itemsData = await vidApi.getCheckpoints(forceRefresh);
} else {
itemsData = await wanvidApi.getLoras(forceRefresh);
itemsData = await vidApi.getLoras(forceRefresh);
}
// Convert rgthreeApi format to our expected format
// Convert vidApi format to our expected format
if (!itemsData || !Array.isArray(itemsData)) {
this.caches[cacheKey] = [];
} else {
@@ -228,7 +228,6 @@ class ModelDataManager {
// 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];
}
@@ -417,7 +416,6 @@ class ModelDataManager {
return jsonData;
}
} catch (attemptError) {
console.log(`[PullInfo Debug] JSON attempt failed: ${attemptError}`);
continue;
}
}
+7 -6
View File
@@ -113,7 +113,6 @@ export async function renameLoRAFiles(loraName, variant = 'high', refreshCallbac
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);
@@ -131,6 +130,8 @@ export async function renameLoRAFiles(loraName, variant = 'high', refreshCallbac
widget.value.lora_name = data.new_low_path;
}
} else {
// The displayed/serialized value is `lora` (not lora_name)
widget.value.lora = data.new_high_path;
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 &&
@@ -139,6 +140,11 @@ export async function renameLoRAFiles(loraName, variant = 'high', refreshCallbac
}
}
// Re-check low variant detection for the new name
if (typeof widget.checkLowLoraVariant === 'function') {
widget.checkLowLoraVariant(widget.value.lora);
}
// Update the displayed name
if (widgetCallback) {
widgetCallback(widget.value, null, null, null);
@@ -151,34 +157,29 @@ export async function renameLoRAFiles(loraName, variant = 'high', refreshCallbac
// 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);
}
@@ -1,4 +1,4 @@
class RgthreeApi {
class QQApi {
constructor(baseUrl) {
this.getCheckpointsPromise = null;
this.getSamplersPromise = null;
@@ -38,7 +38,7 @@ class RgthreeApi {
async postJson(route, json) {
const body = new FormData();
body.append("json", JSON.stringify(json));
return await rgthreeApi.fetchJson(route, { method: "POST", body });
return await vidApi.fetchJson(route, { method: "POST", body });
}
getLoras(force = false) {
if (!this.getLorasPromise || force) {
@@ -168,4 +168,4 @@ class RgthreeApi {
return fetch(url, options);
}
}
export const rgthreeApi = new RgthreeApi();
export const vidApi = new QQApi();
+93 -12
View File
@@ -2,8 +2,6 @@
import { app } from "../../../scripts/app.js";
import { loraPatternMatcher } from "./lora_pattern_matcher.js";
// From rgthree/src_web/comfyui/utils_canvas.ts
export function fitString(ctx, str, maxWidth) {
let width = ctx.measureText(str).width;
const ellipsis = "…";
@@ -83,9 +81,81 @@ export function drawWidgetButton(ctx, pos, size, text, isMouseDownedAndOver) {
ctx.restore();
}
// From rgthree/src_web/comfyui/utils_widgets.ts
// arrows. direction = -1 lays it out right-to-left (grows leftwards from posX).
// Returns [leftArrow, number, rightArrow] hit-area bounds.
export function drawNumberWidgetPart(ctx, options) {
const arrowWidth = 9;
const arrowHeight = 10;
const innerMargin = 3;
const numberWidth = 32;
const xBoundsArrowLess = [0, 0];
const xBoundsNumber = [0, 0];
const xBoundsArrowMore = [0, 0];
ctx.save();
let posX = options.posX;
const { posY, height, value, textColor } = options;
const midY = posY + height / 2;
if (options.direction === -1) {
posX = posX - arrowWidth - innerMargin - numberWidth - innerMargin - arrowWidth;
}
ctx.fill(new Path2D(`M ${posX} ${midY} l ${arrowWidth} ${arrowHeight / 2} l 0 -${arrowHeight} L ${posX} ${midY} z`));
xBoundsArrowLess[0] = posX;
xBoundsArrowLess[1] = arrowWidth;
posX += arrowWidth + innerMargin;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const oldTextColor = ctx.fillStyle;
if (textColor) {
ctx.fillStyle = textColor;
}
ctx.fillText(fitString(ctx, value.toFixed(2), numberWidth), posX + numberWidth / 2, midY);
ctx.fillStyle = oldTextColor;
xBoundsNumber[0] = posX;
xBoundsNumber[1] = numberWidth;
posX += numberWidth + innerMargin;
ctx.fill(new Path2D(`M ${posX} ${midY - arrowHeight / 2} l ${arrowWidth} ${arrowHeight / 2} l -${arrowWidth} ${arrowHeight / 2} v -${arrowHeight} z`));
xBoundsArrowMore[0] = posX;
xBoundsArrowMore[1] = arrowWidth;
ctx.restore();
return [xBoundsArrowLess, xBoundsNumber, xBoundsArrowMore];
}
drawNumberWidgetPart.WIDTH_TOTAL = 9 + 3 + 32 + 3 + 9;
export class RgthreeBaseWidget {
export function drawInfoIcon(ctx, x, y, size = 12, treatment = 'GRAYED') {
ctx.save();
ctx.beginPath();
ctx.roundRect(x, y, size, size, [size * 0.1]);
if (treatment === 'GRAYED') {
ctx.fillStyle = "#aaa";
ctx.strokeStyle = "#aaa";
} else {
ctx.fillStyle = "#2f82ec";
ctx.strokeStyle = "#2f82ec";
}
if (treatment === 'FILLED') {
ctx.fill();
} else {
ctx.stroke();
}
ctx.strokeStyle = "#FFF";
ctx.lineWidth = 2;
const midX = x + size / 2;
const serifSize = size * 0.175;
ctx.stroke(new Path2D(`
M ${midX} ${y + size * 0.15}
v 2
M ${midX - serifSize} ${y + size * 0.45}
h ${serifSize}
v ${size * 0.325}
h ${serifSize}
h -${serifSize * 2}
`));
ctx.restore();
}
export class QQBaseWidget {
constructor(name) {
this.name = name;
this.type = "custom";
@@ -195,7 +265,7 @@ export class RgthreeBaseWidget {
onMouseMove(event, pos, node) { return true; }
}
export class RgthreeBetterButtonWidget extends RgthreeBaseWidget {
export class QQBetterButtonWidget extends QQBaseWidget {
constructor(name, callback, label) {
super(name);
this.callback = callback;
@@ -212,7 +282,7 @@ export class RgthreeBetterButtonWidget extends RgthreeBaseWidget {
}
}
export class RgthreeDividerWidget extends RgthreeBaseWidget {
export class QQDividerWidget extends QQBaseWidget {
constructor(options) {
super("divider");
this.value = {};
@@ -247,7 +317,7 @@ const STRENGTH_WIDTH = 60;
const ICON_WIDTH = 20;
const MARGIN = 10;
export class StrengthCopyWidget extends RgthreeBaseWidget {
export class StrengthCopyWidget extends QQBaseWidget {
constructor(showOutputBoth = true, showCopyButtons = true) {
super("StrengthCopyWidget");
this.showOutputBoth = showOutputBoth;
@@ -397,7 +467,7 @@ export class StrengthCopyWidget extends RgthreeBaseWidget {
}
}
export class CombinedOptionsWidget extends RgthreeBaseWidget {
export class CombinedOptionsWidget extends QQBaseWidget {
constructor() {
super("CombinedOptionsWidget");
this.value = {};
@@ -609,7 +679,7 @@ export class CombinedOptionsWidget extends RgthreeBaseWidget {
}
}
export class PowerLoraLoaderHeaderWidget extends RgthreeBaseWidget {
export class PowerLoraLoaderHeaderWidget extends QQBaseWidget {
constructor() {
super("PowerLoraLoaderHeaderWidget");
this.value = { type: "PowerLoraLoaderHeaderWidget" };
@@ -645,7 +715,7 @@ export class PowerLoraLoaderHeaderWidget extends RgthreeBaseWidget {
}
}
export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
export class PowerLoraLoaderWidget extends QQBaseWidget {
constructor(name, showLoraChooser) {
super(name);
this.showLoraChooser = showLoraChooser;
@@ -657,6 +727,7 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
strength: { bounds: [0, 0], onClick: this.onStrengthClick, onMove: this.onStrengthAnyMove },
low_strength: { bounds: [0, 0], onClick: this.onLowStrengthClick, onMove: this.onLowStrengthAnyMove },
low_variant_icon: { bounds: [0, 0] },
info: { bounds: [0, 0], onDown: this.onInfoDown },
};
}
@@ -743,6 +814,12 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
ctx.fillText(this.value.strength.toFixed(2), rposX, y + h / 2);
rposX -= STRENGTH_WIDTH + MARGIN;
// "i" info badge before (left of) the High strength value.
const infoIconSize = h * 0.66;
drawInfoIcon(ctx, rposX - infoIconSize, y + (h - infoIconSize) / 2, infoIconSize, "GRAYED");
this.hitAreas.info.bounds = [rposX - infoIconSize - MARGIN * 0.33, infoIconSize + MARGIN * 0.66];
rposX -= infoIconSize + MARGIN * 0.66 + MARGIN * 0.33;
const loraWidth = rposX - posX;
this.hitAreas.lora.bounds = [posX, loraWidth];
ctx.textAlign = "left";
@@ -758,6 +835,12 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
return true;
}
onInfoDown(event, pos, node) {
if (!this.value.lora || this.value.lora === "None") return;
this.cancelMouseDown();
this.showLoraInfoDialog('high');
}
setLora(lora) {
// Store the original lora path/name as received from the picker
this.value.lora = lora;
@@ -848,11 +931,9 @@ export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
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
console.log("LoRA info was modified");
}
}));
}).catch(error => {
+2 -2
View File
@@ -2,7 +2,7 @@
import {
drawWidgetButton,
drawNumberWidgetPart,
RgthreeBaseWidget
QQBaseWidget
} from './drawing_utils.js';
import { app } from '../../../scripts/app.js';
@@ -541,7 +541,7 @@ export async function handleFramesRefresh(node) {
// == TOP ROW WIDGET
export class TopRowWidget extends RgthreeBaseWidget {
export class TopRowWidget extends QQBaseWidget {
constructor(name = "TopRowWidget", controlsVisibility = {}, handlers = {}) {
super(name);
this.type = "custom";
+1 -1
View File
@@ -183,7 +183,7 @@ export function drawNumberWidgetPart(ctx, options) {
drawNumberWidgetPart.WIDTH_TOTAL = 9 + 3 + 32 + 3 + 9;
// === BASE WIDGET CLASS ===
export class RgthreeBaseWidget {
export class QQBaseWidget {
constructor(name) {
this.type = "custom";
this.options = {};
+3 -3
View File
@@ -1,12 +1,12 @@
// Button bar widget for adding layers (Add Spline, Add Keyframes, Draw, Duplicate)
import { RgthreeBaseWidget, drawWidgetButton } from './drawing_utils.js';
import { QQBaseWidget, drawWidgetButton } from './drawing_utils.js';
/**
* Creates a button bar widget with Add Spline, Add Keyframes, Draw, and Duplicate buttons
* @returns {RgthreeBaseWidget} The configured button bar widget
* @returns {QQBaseWidget} The configured button bar widget
*/
export function createButtonBarWidget() {
const buttonBarWidget = new RgthreeBaseWidget("button_bar");
const buttonBarWidget = new QQBaseWidget("button_bar");
buttonBarWidget.type = "custom";
buttonBarWidget.serialize = false;
+2 -2
View File
@@ -1,8 +1,8 @@
// Header widget that displays column labels for spline layers
import { app } from '../../../scripts/app.js';
import { isLowQuality, drawTogglePart, drawNumberWidgetPart, RgthreeBaseWidget } from './drawing_utils.js';
import { isLowQuality, drawTogglePart, drawNumberWidgetPart, QQBaseWidget } from './drawing_utils.js';
export class PowerSplineHeaderWidget extends RgthreeBaseWidget {
export class PowerSplineHeaderWidget extends QQBaseWidget {
constructor(name = "PowerSplineHeaderWidget") {
super(name);
this.value = { type: "PowerSplineHeaderWidget" };
+2 -2
View File
@@ -1,11 +1,11 @@
import { RgthreeBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { QQBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { initializeDrivenConfig, initializeEasingConfig, initializeScaleConfig, toggleDrivenState, prepareDrivenMenu } from './persistence.js';
import { showCustomEasingMenu, showCustomDrivenToggleMenu, showInterpolationMenu, showRefSelectionMenu } from './context_menu.js';
import { app } from '../../../scripts/app.js';
const BOX_TIMELINE_MAX_POINTS = 250;
export class BoxLayerWidget extends RgthreeBaseWidget {
export class BoxLayerWidget extends QQBaseWidget {
constructor(name) {
super(name);
this.type = 'custom';
+2 -2
View File
@@ -1,10 +1,10 @@
// Hand-draw layer widget and helpers
import { RgthreeBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { QQBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { initializeDrivenConfig, initializeEasingConfig, initializeScaleConfig, toggleDrivenState, prepareDrivenMenu } from './persistence.js';
import { showCustomEasingMenu, showCustomDrivenToggleMenu } from './context_menu.js';
import { app } from '../../../scripts/app.js';
export class HandDrawLayerWidget extends RgthreeBaseWidget {
export class HandDrawLayerWidget extends QQBaseWidget {
constructor(name) {
super(name);
this.type = 'custom';
+2 -2
View File
@@ -1,8 +1,8 @@
// Simple reference layer widget
import { RgthreeBaseWidget, drawRoundedRectangle } from './drawing_utils.js';
import { QQBaseWidget, drawRoundedRectangle } from './drawing_utils.js';
import { app } from '../../../scripts/app.js';
export class RefLayerWidget extends RgthreeBaseWidget {
export class RefLayerWidget extends QQBaseWidget {
constructor(name) {
super(name);
this.type = "custom";
+2 -2
View File
@@ -1,10 +1,10 @@
// Normal spline layer widget
import { RgthreeBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { QQBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { initializeDrivenConfig, initializeEasingConfig, initializeScaleConfig, toggleDrivenState, prepareDrivenMenu } from './persistence.js';
import { showCustomEasingMenu, showCustomDrivenToggleMenu, showInterpolationMenu } from './context_menu.js';
import { app } from '../../../scripts/app.js';
export class PowerSplineWidget extends RgthreeBaseWidget {
export class PowerSplineWidget extends QQBaseWidget {
constructor(name) {
super(name);
this.type = "custom";
@@ -8,7 +8,7 @@ import {
makeUUID,
loadScript,
create_documentation_stylesheet,
RgthreeBaseWidget,
QQBaseWidget,
DimensionsWidget,
PowerSplineHeaderWidget,
NodeSizeManager,
+7 -7
View File
@@ -16,7 +16,7 @@ import {
drawWidgetButton,
drawTogglePart,
drawNumberWidgetPart,
RgthreeBaseWidget
QQBaseWidget
} from './drawing_utils.js';
// Re-export for convenience
@@ -35,7 +35,7 @@ export {
drawWidgetButton,
drawTogglePart,
drawNumberWidgetPart,
RgthreeBaseWidget
QQBaseWidget
};
/**
@@ -200,7 +200,7 @@ export const create_documentation_stylesheet = () => {
}
// === DIMENSIONS WIDGET (Simplified version for backward compatibility) ===
export class DimensionsWidget extends RgthreeBaseWidget {
export class DimensionsWidget extends QQBaseWidget {
constructor(name = "DimensionsWidget") {
super(name);
this.type = "custom";
@@ -321,7 +321,7 @@ export class DimensionsWidget extends RgthreeBaseWidget {
}
// === INTERPOLATION WIDGET ===
export class InterpolationWidget extends RgthreeBaseWidget {
export class InterpolationWidget extends QQBaseWidget {
constructor(name = "InterpolationWidget") {
super(name);
this.type = "custom";
@@ -437,7 +437,7 @@ export class InterpolationWidget extends RgthreeBaseWidget {
}
// === DRIVER ROTATION & D_SCALE WIDGET (Combined driver_rotation and driver_d_scale) ===
export class DriverRotationDScaleWidget extends RgthreeBaseWidget {
export class DriverRotationDScaleWidget extends QQBaseWidget {
constructor(name = "DriverRotationDScaleWidget") {
super(name);
this.type = "custom";
@@ -624,7 +624,7 @@ export class DriverRotationDScaleWidget extends RgthreeBaseWidget {
}
// === PAUSE WIDGET (Combined start_pause and end_pause) ===
export class PauseWidget extends RgthreeBaseWidget {
export class PauseWidget extends QQBaseWidget {
constructor(name = "PauseWidget") {
super(name);
this.type = "custom";
@@ -812,7 +812,7 @@ export class PauseWidget extends RgthreeBaseWidget {
}
// === OFFSET REPEAT WIDGET (Combined offset and repeat) ===
export class OffsetRepeatWidget extends RgthreeBaseWidget {
export class OffsetRepeatWidget extends QQBaseWidget {
constructor(name = "OffsetRepeatWidget") {
super(name);
this.type = "custom";
@@ -1,7 +1,4 @@
// Enhanced rgthree implementation for WanVideoWrapper QQ
// Provides proper toast messages and development utilities
export const rgthree = {
export const qq = {
showMessage: function(options) {
if (typeof options === 'string') {
// If it's just a string, convert to options object