mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3079131337 | ||
|
|
a34ade0120 | ||
|
|
e9ada70088 | ||
|
|
597cc48248 | ||
|
|
ec3f857ef1 | ||
|
|
383b4de539 | ||
|
|
1bf9326604 | ||
|
|
d9f5459d46 | ||
|
|
e45a1b1e19 | ||
|
|
331ad8f644 | ||
|
|
52fa88b04c | ||
|
|
8895a64d24 | ||
|
|
fdec535559 |
@@ -34,6 +34,11 @@ Enhance your Civitai browsing experience with our companion browser extension! S
|
||||
|
||||
## Release Notes
|
||||
|
||||
### v0.8.30
|
||||
* **Automatic Model Path Correction** - Added auto-correction for model paths in built-in nodes such as Load Checkpoint, Load Diffusion Model, Load LoRA, and other custom nodes with similar functionality. Workflows containing outdated or incorrect model paths will now be automatically updated to reflect the current location of your models.
|
||||
* **Node UI Enhancements** - Improved node interface for a smoother and more intuitive user experience.
|
||||
* **Bug Fixes** - Addressed various bugs to enhance stability and reliability.
|
||||
|
||||
### v0.8.29
|
||||
* **Enhanced Recipe Imports** - Improved recipe importing with new target folder selection, featuring path input autocomplete and interactive folder tree navigation. Added a "Use Default Path" option when downloading missing LoRAs.
|
||||
* **WanVideo Lora Select Node Update** - Updated the WanVideo Lora Select node with a 'merge_loras' option to match the counterpart node in the WanVideoWrapper node package.
|
||||
|
||||
@@ -644,6 +644,7 @@ NODE_EXTRACTORS = {
|
||||
"KSamplerAdvanced": KSamplerAdvancedExtractor,
|
||||
"SamplerCustom": KSamplerAdvancedExtractor,
|
||||
"SamplerCustomAdvanced": SamplerCustomAdvancedExtractor,
|
||||
"ClownsharKSampler_Beta": SamplerExtractor,
|
||||
"TSC_KSampler": TSCKSamplerExtractor, # Efficient Nodes
|
||||
"TSC_KSamplerAdvanced": TSCKSamplerAdvancedExtractor, # Efficient Nodes
|
||||
"KSamplerBasicPipe": KSamplerBasicPipeExtractor, # comfyui-impact-pack
|
||||
|
||||
@@ -398,12 +398,12 @@ class BaseModelService(ABC):
|
||||
relative_path = None
|
||||
for root in model_roots:
|
||||
# Normalize paths for comparison
|
||||
normalized_root = os.path.normpath(root).replace(os.sep, '/')
|
||||
normalized_file = os.path.normpath(file_path).replace(os.sep, '/')
|
||||
normalized_root = os.path.normpath(root)
|
||||
normalized_file = os.path.normpath(file_path)
|
||||
|
||||
if normalized_file.startswith(normalized_root):
|
||||
# Remove root and leading slash to get relative path
|
||||
relative_path = normalized_file[len(normalized_root):].lstrip('/')
|
||||
# Remove root and leading separator to get relative path
|
||||
relative_path = normalized_file[len(normalized_root):].lstrip(os.sep)
|
||||
break
|
||||
|
||||
if relative_path and search_lower in relative_path.lower():
|
||||
|
||||
@@ -167,6 +167,7 @@ class LoraService(BaseModelService):
|
||||
if file_path:
|
||||
# Convert to forward slashes and extract relative path
|
||||
file_path_normalized = file_path.replace('\\', '/')
|
||||
relative_path = relative_path.replace('\\', '/')
|
||||
# Find the relative path part by looking for the relative_path in the full path
|
||||
if file_path_normalized.endswith(relative_path) or relative_path in file_path_normalized:
|
||||
return lora.get('usage_tips', '')
|
||||
|
||||
@@ -110,6 +110,43 @@ class SettingsManager:
|
||||
Template string for the model type, defaults to '{base_model}/{first_tag}'
|
||||
"""
|
||||
templates = self.settings.get('download_path_templates', {})
|
||||
|
||||
# Handle edge case where templates might be stored as JSON string
|
||||
if isinstance(templates, str):
|
||||
try:
|
||||
# Try to parse JSON string
|
||||
parsed_templates = json.loads(templates)
|
||||
if isinstance(parsed_templates, dict):
|
||||
# Update settings with parsed dictionary
|
||||
self.settings['download_path_templates'] = parsed_templates
|
||||
self._save_settings()
|
||||
templates = parsed_templates
|
||||
logger.info("Successfully parsed download_path_templates from JSON string")
|
||||
else:
|
||||
raise ValueError("Parsed JSON is not a dictionary")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
# If parsing fails, set default values
|
||||
logger.warning(f"Failed to parse download_path_templates JSON string: {e}. Setting default values.")
|
||||
default_template = '{base_model}/{first_tag}'
|
||||
templates = {
|
||||
'lora': default_template,
|
||||
'checkpoint': default_template,
|
||||
'embedding': default_template
|
||||
}
|
||||
self.settings['download_path_templates'] = templates
|
||||
self._save_settings()
|
||||
|
||||
# Ensure templates is a dictionary
|
||||
if not isinstance(templates, dict):
|
||||
default_template = '{base_model}/{first_tag}'
|
||||
templates = {
|
||||
'lora': default_template,
|
||||
'checkpoint': default_template,
|
||||
'embedding': default_template
|
||||
}
|
||||
self.settings['download_path_templates'] = templates
|
||||
self._save_settings()
|
||||
|
||||
return templates.get(model_type, '{base_model}/{first_tag}')
|
||||
|
||||
settings = SettingsManager()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-lora-manager"
|
||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||
version = "0.8.29"
|
||||
version = "0.8.30"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
|
||||
@@ -23,7 +23,7 @@ body.modal-open {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
height: auto;
|
||||
/* max-height: calc(90vh - 48px); */
|
||||
max-height: calc(90vh);
|
||||
margin: 1rem auto; /* Keep reduced top margin */
|
||||
background: var(--lora-surface);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
@@ -45,6 +45,9 @@ export const BASE_MODELS = {
|
||||
WAN_VIDEO_14B_T2V: "Wan Video 14B t2v",
|
||||
WAN_VIDEO_14B_I2V_480P: "Wan Video 14B i2v 480p",
|
||||
WAN_VIDEO_14B_I2V_720P: "Wan Video 14B i2v 720p",
|
||||
WAN_VIDEO_2_2_TI2V_5B: "Wan Video 2.2 TI2V-5B",
|
||||
WAN_VIDEO_2_2_T2V_A14B: "Wan Video 2.2 T2V-A14B",
|
||||
WAN_VIDEO_2_2_I2V_A14B: "Wan Video 2.2 I2V-A14B",
|
||||
HUNYUAN_VIDEO: "Hunyuan Video",
|
||||
// Default
|
||||
UNKNOWN: "Other"
|
||||
|
||||
@@ -248,7 +248,7 @@ class AutoComplete {
|
||||
if (!this.previewTooltip) return;
|
||||
|
||||
// Extract filename without extension for preview
|
||||
const fileName = relativePath.split('/').pop();
|
||||
const fileName = relativePath.split(/[/\\]/).pop();
|
||||
const loraName = fileName.replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
|
||||
|
||||
// Get item position for tooltip positioning
|
||||
@@ -256,7 +256,7 @@ class AutoComplete {
|
||||
const x = rect.right + 10;
|
||||
const y = rect.top;
|
||||
|
||||
this.previewTooltip.show(loraName, x, y);
|
||||
this.previewTooltip.show(loraName, x, y, true); // Pass true for fromAutocomplete flag
|
||||
}
|
||||
|
||||
hidePreview() {
|
||||
@@ -380,7 +380,7 @@ class AutoComplete {
|
||||
|
||||
async insertSelection(relativePath) {
|
||||
// Extract just the filename for LoRA name
|
||||
const fileName = relativePath.split('/').pop().replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
|
||||
const fileName = relativePath.split(/[/\\]/).pop().replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
|
||||
|
||||
// Get usage tips and extract strength
|
||||
let strength = 1.0; // Default strength
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection } from "./loras_widget_components.js";
|
||||
import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection, createExpandButton, updateExpandButtonState } from "./loras_widget_components.js";
|
||||
import {
|
||||
parseLoraValue,
|
||||
formatLoraValue,
|
||||
@@ -215,7 +215,8 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
if (e.target.closest('.comfy-lora-toggle') ||
|
||||
e.target.closest('input') ||
|
||||
e.target.closest('.comfy-lora-arrow') ||
|
||||
e.target.closest('.comfy-lora-drag-handle')) {
|
||||
e.target.closest('.comfy-lora-drag-handle') ||
|
||||
e.target.closest('.comfy-lora-expand-button')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,41 +226,6 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
container.focus(); // Focus container for keyboard events
|
||||
});
|
||||
|
||||
// Add double-click handler to toggle clip entry
|
||||
loraEl.addEventListener('dblclick', (e) => {
|
||||
// Skip if clicking on toggle or strength control areas
|
||||
if (e.target.closest('.comfy-lora-toggle') ||
|
||||
e.target.closest('input') ||
|
||||
e.target.closest('.comfy-lora-arrow')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default behavior
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Toggle the clip entry expanded state
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||
|
||||
if (loraIndex >= 0) {
|
||||
// Explicitly toggle the expansion state
|
||||
const currentExpanded = shouldShowClipEntry(lorasData[loraIndex]);
|
||||
lorasData[loraIndex].expanded = !currentExpanded;
|
||||
|
||||
// If collapsing, set clipStrength = strength
|
||||
if (!lorasData[loraIndex].expanded) {
|
||||
lorasData[loraIndex].clipStrength = lorasData[loraIndex].strength;
|
||||
}
|
||||
|
||||
// Update the widget value
|
||||
widget.value = formatLoraValue(lorasData);
|
||||
|
||||
// Re-render to show/hide clip entry
|
||||
renderLoras(widget.value, widget);
|
||||
}
|
||||
});
|
||||
|
||||
// Create drag handle for reordering
|
||||
const dragHandle = createDragHandle();
|
||||
|
||||
@@ -280,32 +246,46 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
}
|
||||
});
|
||||
|
||||
// Create expand button
|
||||
const expandButton = createExpandButton(isExpanded, (shouldExpand) => {
|
||||
// Toggle the clip entry expanded state
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||
|
||||
if (loraIndex >= 0) {
|
||||
// Set the expansion state
|
||||
lorasData[loraIndex].expanded = shouldExpand;
|
||||
|
||||
// If collapsing, set clipStrength = strength
|
||||
if (!shouldExpand) {
|
||||
lorasData[loraIndex].clipStrength = lorasData[loraIndex].strength;
|
||||
}
|
||||
|
||||
// Update the widget value
|
||||
widget.value = formatLoraValue(lorasData);
|
||||
|
||||
// Re-render to show/hide clip entry
|
||||
renderLoras(widget.value, widget);
|
||||
}
|
||||
});
|
||||
|
||||
// Create name display
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.textContent = name;
|
||||
Object.assign(nameEl.style, {
|
||||
marginLeft: "10px",
|
||||
marginLeft: "4px",
|
||||
flex: "1",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)",
|
||||
fontSize: "13px",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
});
|
||||
|
||||
// Add expand indicator to name element
|
||||
const expandIndicator = document.createElement("span");
|
||||
expandIndicator.textContent = isExpanded ? " ▼" : " ▶";
|
||||
expandIndicator.style.opacity = "0.7";
|
||||
expandIndicator.style.fontSize = "9px";
|
||||
expandIndicator.style.marginLeft = "4px";
|
||||
nameEl.appendChild(expandIndicator);
|
||||
|
||||
// Move preview tooltip events to nameEl instead of loraEl
|
||||
nameEl.addEventListener('mouseenter', async (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -464,6 +444,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
|
||||
leftSection.appendChild(dragHandle); // Add drag handle first
|
||||
leftSection.appendChild(toggle);
|
||||
leftSection.appendChild(expandButton); // Add expand button
|
||||
leftSection.appendChild(nameEl);
|
||||
|
||||
loraEl.appendChild(leftSection);
|
||||
@@ -471,9 +452,6 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
|
||||
container.appendChild(loraEl);
|
||||
|
||||
// Update selection state
|
||||
updateEntrySelection(loraEl, name === selectedLora);
|
||||
|
||||
// If expanded, show the clip entry
|
||||
if (isExpanded) {
|
||||
totalVisibleEntries++;
|
||||
@@ -657,6 +635,13 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
// Calculate height based on number of loras and fixed sizes
|
||||
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 12) * LORA_ENTRY_HEIGHT);
|
||||
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
|
||||
|
||||
// After all LoRA elements are created, apply selection state as the last step
|
||||
// This ensures the selection state is not overwritten
|
||||
container.querySelectorAll('.comfy-lora-entry').forEach(entry => {
|
||||
const entryLoraName = entry.dataset.loraName;
|
||||
updateEntrySelection(entry, entryLoraName === selectedLora);
|
||||
});
|
||||
};
|
||||
|
||||
// Store the value in a variable to avoid recursion
|
||||
|
||||
@@ -115,6 +115,10 @@ export function createDragHandle() {
|
||||
handle.onmousedown = () => {
|
||||
handle.style.cursor = "grabbing";
|
||||
};
|
||||
|
||||
handle.onmouseup = () => {
|
||||
handle.style.cursor = "grab";
|
||||
};
|
||||
|
||||
return handle;
|
||||
}
|
||||
@@ -143,15 +147,22 @@ export function createDropIndicator() {
|
||||
|
||||
// Function to update entry selection state
|
||||
export function updateEntrySelection(entryEl, isSelected) {
|
||||
const baseColor = entryEl.dataset.active === 'true' ?
|
||||
// Remove any conflicting styles first
|
||||
entryEl.style.removeProperty('border');
|
||||
entryEl.style.removeProperty('box-shadow');
|
||||
|
||||
const baseColor = entryEl.dataset.active === 'true' ?
|
||||
"rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)";
|
||||
const selectedColor = entryEl.dataset.active === 'true' ?
|
||||
const selectedColor = entryEl.dataset.active === 'true' ?
|
||||
"rgba(66, 153, 225, 0.3)" : "rgba(66, 153, 225, 0.2)";
|
||||
|
||||
// Update data attribute to track selection state
|
||||
entryEl.dataset.selected = isSelected ? 'true' : 'false';
|
||||
|
||||
if (isSelected) {
|
||||
entryEl.style.backgroundColor = selectedColor;
|
||||
entryEl.style.border = "1px solid rgba(66, 153, 225, 0.6)";
|
||||
entryEl.style.boxShadow = "0 0 0 1px rgba(66, 153, 225, 0.3)";
|
||||
entryEl.style.setProperty('backgroundColor', selectedColor, 'important');
|
||||
entryEl.style.setProperty('border', "1px solid rgba(66, 153, 225, 0.6)", 'important');
|
||||
entryEl.style.setProperty('box-shadow', "0 0 0 1px rgba(66, 153, 225, 0.3)", 'important');
|
||||
} else {
|
||||
entryEl.style.backgroundColor = baseColor;
|
||||
entryEl.style.border = "1px solid transparent";
|
||||
@@ -301,8 +312,6 @@ export class PreviewTooltip {
|
||||
mediaElement.controls = false;
|
||||
}
|
||||
|
||||
mediaElement.src = data.preview_url;
|
||||
|
||||
// Create name label with absolute positioning
|
||||
const nameLabel = document.createElement('div');
|
||||
nameLabel.textContent = loraName;
|
||||
@@ -328,12 +337,43 @@ export class PreviewTooltip {
|
||||
mediaContainer.appendChild(nameLabel);
|
||||
this.element.appendChild(mediaContainer);
|
||||
|
||||
// Add fade-in effect
|
||||
// Show element with opacity 0 first to get dimensions
|
||||
this.element.style.opacity = '0';
|
||||
this.element.style.display = 'block';
|
||||
this.position(x, y);
|
||||
|
||||
// Wait for media to load before positioning
|
||||
const waitForLoad = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (isVideo) {
|
||||
if (mediaElement.readyState >= 2) { // HAVE_CURRENT_DATA
|
||||
resolve();
|
||||
} else {
|
||||
mediaElement.addEventListener('loadeddata', resolve, { once: true });
|
||||
mediaElement.addEventListener('error', resolve, { once: true });
|
||||
}
|
||||
} else {
|
||||
if (mediaElement.complete) {
|
||||
resolve();
|
||||
} else {
|
||||
mediaElement.addEventListener('load', resolve, { once: true });
|
||||
mediaElement.addEventListener('error', resolve, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Set a timeout to prevent hanging
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
// Set source after setting up load listeners
|
||||
mediaElement.src = data.preview_url;
|
||||
|
||||
// Wait for content to load, then position and show
|
||||
await waitForLoad();
|
||||
|
||||
// Small delay to ensure layout is complete
|
||||
requestAnimationFrame(() => {
|
||||
this.position(x, y);
|
||||
this.element.style.transition = 'opacity 0.15s ease';
|
||||
this.element.style.opacity = '1';
|
||||
});
|
||||
@@ -399,3 +439,86 @@ export class PreviewTooltip {
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create expand/collapse button
|
||||
export function createExpandButton(isExpanded, onClick) {
|
||||
const button = document.createElement("button");
|
||||
button.className = "comfy-lora-expand-button";
|
||||
button.type = "button";
|
||||
|
||||
Object.assign(button.style, {
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
fontSize: "10px",
|
||||
color: "rgba(226, 232, 240, 0.7)",
|
||||
backgroundColor: "rgba(45, 55, 72, 0.3)",
|
||||
border: "1px solid rgba(226, 232, 240, 0.2)",
|
||||
borderRadius: "3px",
|
||||
transition: "all 0.2s ease",
|
||||
marginLeft: "6px",
|
||||
marginRight: "4px",
|
||||
flexShrink: "0",
|
||||
outline: "none"
|
||||
});
|
||||
|
||||
// Set icon based on expanded state
|
||||
updateExpandButtonState(button, isExpanded);
|
||||
|
||||
button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick(!isExpanded);
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
button.addEventListener("mouseenter", () => {
|
||||
button.style.backgroundColor = "rgba(66, 153, 225, 0.2)";
|
||||
button.style.borderColor = "rgba(66, 153, 225, 0.4)";
|
||||
button.style.color = "rgba(226, 232, 240, 0.9)";
|
||||
button.style.transform = "scale(1.05)";
|
||||
});
|
||||
|
||||
button.addEventListener("mouseleave", () => {
|
||||
button.style.backgroundColor = "rgba(45, 55, 72, 0.3)";
|
||||
button.style.borderColor = "rgba(226, 232, 240, 0.2)";
|
||||
button.style.color = "rgba(226, 232, 240, 0.7)";
|
||||
button.style.transform = "scale(1)";
|
||||
});
|
||||
|
||||
// Add active (pressed) state
|
||||
button.addEventListener("mousedown", () => {
|
||||
button.style.transform = "scale(0.95)";
|
||||
button.style.backgroundColor = "rgba(66, 153, 225, 0.3)";
|
||||
});
|
||||
|
||||
button.addEventListener("mouseup", () => {
|
||||
button.style.transform = "scale(1.05)"; // Return to hover state
|
||||
});
|
||||
|
||||
// Add focus state for keyboard accessibility
|
||||
button.addEventListener("focus", () => {
|
||||
button.style.boxShadow = "0 0 0 2px rgba(66, 153, 225, 0.5)";
|
||||
});
|
||||
|
||||
button.addEventListener("blur", () => {
|
||||
button.style.boxShadow = "none";
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Helper function to update expand button state
|
||||
export function updateExpandButtonState(button, isExpanded) {
|
||||
if (isExpanded) {
|
||||
button.innerHTML = "▼"; // Down arrow for expanded
|
||||
button.title = "Collapse clip controls";
|
||||
} else {
|
||||
button.innerHTML = "▶"; // Right arrow for collapsed
|
||||
button.title = "Expand clip controls";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,9 @@ export function initDrag(dragEl, name, widget, isClipStrength = false, previewTo
|
||||
// Skip if clicking on toggle or strength control areas
|
||||
if (e.target.closest('.comfy-lora-toggle') ||
|
||||
e.target.closest('input') ||
|
||||
e.target.closest('.comfy-lora-arrow')) {
|
||||
e.target.closest('.comfy-lora-arrow') ||
|
||||
e.target.closest('.comfy-lora-drag-handle') ||
|
||||
e.target.closest('.comfy-lora-expand-button')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,6 +306,9 @@ export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
// Always reset cursor regardless of isDragging state
|
||||
document.body.style.cursor = '';
|
||||
|
||||
if (!isDragging || !draggedElement) return;
|
||||
|
||||
const targetIndex = getDropTargetIndex(container, e.clientY);
|
||||
@@ -356,10 +361,13 @@ export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
|
||||
dropIndicator = null;
|
||||
}
|
||||
|
||||
// Reset cursor
|
||||
document.body.style.cursor = '';
|
||||
container = null;
|
||||
});
|
||||
|
||||
// Also reset cursor when mouse leaves the document
|
||||
document.addEventListener('mouseleave', () => {
|
||||
document.body.style.cursor = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle keyboard navigation
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
// ComfyUI extension to track model usage statistics
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { showToast } from "./utils.js";
|
||||
|
||||
// Define target nodes and their widget configurations
|
||||
const PATH_CORRECTION_TARGETS = [
|
||||
{ comfyClass: "CheckpointLoaderSimple", widgetName: "ckpt_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "Checkpoint Loader with Name (Image Saver)", widgetName: "ckpt_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "UNETLoader", widgetName: "unet_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "easy comfyLoader", widgetName: "ckpt_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "CheckpointLoader|pysssss", widgetName: "ckpt_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "Efficient Loader", widgetName: "ckpt_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "UnetLoaderGGUF", widgetName: "unet_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "UnetLoaderGGUFAdvanced", widgetName: "unet_name", modelType: "checkpoints" },
|
||||
{ comfyClass: "LoraLoader", widgetName: "lora_name", modelType: "loras" },
|
||||
{ comfyClass: "easy loraStack", widgetNamePattern: "lora_\\d+_name", modelType: "loras" }
|
||||
];
|
||||
|
||||
// Register the extension
|
||||
app.registerExtension({
|
||||
@@ -80,5 +95,110 @@ app.registerExtension({
|
||||
} catch (error) {
|
||||
console.error("Error refreshing registry:", error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadedGraphNode(node) {
|
||||
// Check if this node type needs path correction
|
||||
const target = PATH_CORRECTION_TARGETS.find(t => t.comfyClass === node.comfyClass);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.correctNodePaths(node, target);
|
||||
},
|
||||
|
||||
async correctNodePaths(node, target) {
|
||||
try {
|
||||
if (target.widgetNamePattern) {
|
||||
// Handle pattern-based widget names (like lora_1_name, lora_2_name, etc.)
|
||||
const pattern = new RegExp(target.widgetNamePattern);
|
||||
const widgetIndexes = [];
|
||||
|
||||
if (node.widgets) {
|
||||
node.widgets.forEach((widget, index) => {
|
||||
if (pattern.test(widget.name)) {
|
||||
widgetIndexes.push(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process each matching widget
|
||||
for (const widgetIndex of widgetIndexes) {
|
||||
await this.correctWidgetPath(node, widgetIndex, target.modelType);
|
||||
}
|
||||
} else {
|
||||
// Handle single widget name
|
||||
if (node.widgets) {
|
||||
const widgetIndex = node.widgets.findIndex(w => w.name === target.widgetName);
|
||||
if (widgetIndex !== -1) {
|
||||
await this.correctWidgetPath(node, widgetIndex, target.modelType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error correcting node paths:", error);
|
||||
}
|
||||
},
|
||||
|
||||
async correctWidgetPath(node, widgetIndex, modelType) {
|
||||
if (!node.widgets_values || !node.widgets_values[widgetIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPath = node.widgets_values[widgetIndex];
|
||||
if (!currentPath || typeof currentPath !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract filename from path (after last separator)
|
||||
const fileName = currentPath.split(/[/\\]/).pop();
|
||||
if (!fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Search for current relative path
|
||||
const response = await api.fetchApi(`/${modelType}/relative-paths?search=${encodeURIComponent(fileName)}&limit=2`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success || !data.relative_paths || data.relative_paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foundPaths = data.relative_paths;
|
||||
const firstPath = foundPaths[0];
|
||||
|
||||
// Check if we need to update the path
|
||||
if (firstPath !== currentPath) {
|
||||
// Update the widget value
|
||||
// node.widgets_values[widgetIndex] = firstPath;
|
||||
node.widgets[widgetIndex].value = firstPath;
|
||||
|
||||
if (foundPaths.length === 1) {
|
||||
// Single match found - success
|
||||
showToast({
|
||||
severity: 'info',
|
||||
summary: 'LoRA Manager Path Correction',
|
||||
detail: `Updated path for ${fileName}: ${firstPath}`,
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
// Multiple matches found - warning
|
||||
showToast({
|
||||
severity: 'warn',
|
||||
summary: 'LoRA Manager Path Correction',
|
||||
detail: `Multiple paths found for ${fileName}, using: ${firstPath}`,
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
|
||||
// Mark node as modified
|
||||
if (node.setDirtyCanvas) {
|
||||
node.setDirtyCanvas(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error correcting path for ${fileName}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,60 @@ export function getComfyUIFrontendVersion() {
|
||||
return window['__COMFYUI_FRONTEND_VERSION__'] || "0.0.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a toast notification
|
||||
* @param {Object|string} options - Toast options object or message string for backward compatibility
|
||||
* @param {string} [options.severity] - Message severity level (success, info, warn, error, secondary, contrast)
|
||||
* @param {string} [options.summary] - Short title for the toast
|
||||
* @param {any} [options.detail] - Detailed message content
|
||||
* @param {boolean} [options.closable] - Whether user can close the toast (default: true)
|
||||
* @param {number} [options.life] - Duration in milliseconds before auto-closing
|
||||
* @param {string} [options.group] - Group identifier for managing related toasts
|
||||
* @param {any} [options.styleClass] - Style class of the message
|
||||
* @param {any} [options.contentStyleClass] - Style class of the content
|
||||
* @param {string} [type] - Deprecated: severity type for backward compatibility
|
||||
*/
|
||||
export function showToast(options, type = 'info') {
|
||||
// Handle backward compatibility: showToast(message, type)
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
detail: options,
|
||||
severity: type
|
||||
};
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
const toastOptions = {
|
||||
severity: options.severity || 'info',
|
||||
summary: options.summary,
|
||||
detail: options.detail,
|
||||
closable: options.closable !== false, // default to true
|
||||
life: options.life,
|
||||
group: options.group,
|
||||
styleClass: options.styleClass,
|
||||
contentStyleClass: options.contentStyleClass
|
||||
};
|
||||
|
||||
// Remove undefined properties
|
||||
Object.keys(toastOptions).forEach(key => {
|
||||
if (toastOptions[key] === undefined) {
|
||||
delete toastOptions[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add(toastOptions);
|
||||
} else {
|
||||
const message = toastOptions.detail || toastOptions.summary || 'No message';
|
||||
const severity = toastOptions.severity.toUpperCase();
|
||||
console.log(`${severity}: ${message}`);
|
||||
// Fallback alert for critical errors only
|
||||
if (toastOptions.severity === 'error') {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically import the appropriate widget based on app version
|
||||
export async function dynamicImportByVersion(latestModulePath, legacyModulePath) {
|
||||
// Parse app version and compare with 1.12.6 (version when tags widget API changed)
|
||||
|
||||
Reference in New Issue
Block a user