mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28fe3e7b7a | ||
|
|
c0eff2bb5e | ||
|
|
848c1741fe | ||
|
|
1370b8e8c1 | ||
|
|
82a068e610 |
@@ -59,6 +59,9 @@ class Config:
|
|||||||
|
|
||||||
if self.checkpoints_roots and len(self.checkpoints_roots) == 1 and "default_checkpoint_root" not in settings:
|
if self.checkpoints_roots and len(self.checkpoints_roots) == 1 and "default_checkpoint_root" not in settings:
|
||||||
settings["default_checkpoint_root"] = self.checkpoints_roots[0]
|
settings["default_checkpoint_root"] = self.checkpoints_roots[0]
|
||||||
|
|
||||||
|
if self.embeddings_roots and len(self.embeddings_roots) == 1 and "default_embedding_root" not in settings:
|
||||||
|
settings["default_embedding_root"] = self.embeddings_roots[0]
|
||||||
|
|
||||||
# Save settings
|
# Save settings
|
||||||
with open(settings_path, 'w', encoding='utf-8') as f:
|
with open(settings_path, 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
@@ -146,52 +146,40 @@ class MetadataHook:
|
|||||||
# Store the original _async_map_node_over_list function
|
# Store the original _async_map_node_over_list function
|
||||||
original_map_node_over_list = getattr(execution, map_node_func_name)
|
original_map_node_over_list = getattr(execution, map_node_func_name)
|
||||||
|
|
||||||
# Define the wrapped async function - NOTE: Updated signature with prompt_id and unique_id!
|
# Wrapped async function, compatible with both stable and nightly
|
||||||
async def async_map_node_over_list_with_metadata(prompt_id, unique_id, obj, input_data_all, func, allow_interrupt=False, execution_block_cb=None, pre_execute_cb=None):
|
async def async_map_node_over_list_with_metadata(prompt_id, unique_id, obj, input_data_all, func, allow_interrupt=False, execution_block_cb=None, pre_execute_cb=None, *args, **kwargs):
|
||||||
|
hidden_inputs = kwargs.get('hidden_inputs', None)
|
||||||
# Only collect metadata when calling the main function of nodes
|
# Only collect metadata when calling the main function of nodes
|
||||||
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
||||||
try:
|
try:
|
||||||
# Get the current prompt_id from the registry
|
|
||||||
registry = MetadataRegistry()
|
registry = MetadataRegistry()
|
||||||
# We now have prompt_id directly from the function parameters
|
|
||||||
|
|
||||||
if prompt_id is not None:
|
if prompt_id is not None:
|
||||||
# Get node class type
|
|
||||||
class_type = obj.__class__.__name__
|
class_type = obj.__class__.__name__
|
||||||
|
|
||||||
# Use the passed unique_id parameter instead of trying to extract it
|
|
||||||
node_id = unique_id
|
node_id = unique_id
|
||||||
|
|
||||||
# Record inputs before execution
|
|
||||||
if node_id is not None:
|
if node_id is not None:
|
||||||
registry.record_node_execution(node_id, class_type, input_data_all, None)
|
registry.record_node_execution(node_id, class_type, input_data_all, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error collecting metadata (pre-execution): {str(e)}")
|
print(f"Error collecting metadata (pre-execution): {str(e)}")
|
||||||
|
|
||||||
# Execute the original async function with ALL parameters in the correct order
|
# Call original function with all args/kwargs
|
||||||
results = await original_map_node_over_list(prompt_id, unique_id, obj, input_data_all, func, allow_interrupt, execution_block_cb, pre_execute_cb)
|
results = await original_map_node_over_list(
|
||||||
|
prompt_id, unique_id, obj, input_data_all, func,
|
||||||
|
allow_interrupt, execution_block_cb, pre_execute_cb, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# After execution, collect outputs for relevant nodes
|
|
||||||
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
||||||
try:
|
try:
|
||||||
# Get the current prompt_id from the registry
|
|
||||||
registry = MetadataRegistry()
|
registry = MetadataRegistry()
|
||||||
|
|
||||||
if prompt_id is not None:
|
if prompt_id is not None:
|
||||||
# Get node class type
|
|
||||||
class_type = obj.__class__.__name__
|
class_type = obj.__class__.__name__
|
||||||
|
|
||||||
# Use the passed unique_id parameter
|
|
||||||
node_id = unique_id
|
node_id = unique_id
|
||||||
|
|
||||||
# Record outputs after execution
|
|
||||||
if node_id is not None:
|
if node_id is not None:
|
||||||
registry.update_node_execution(node_id, class_type, results)
|
registry.update_node_execution(node_id, class_type, results)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error collecting metadata (post-execution): {str(e)}")
|
print(f"Error collecting metadata (post-execution): {str(e)}")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# Also hook the execute function to track the current prompt_id
|
# Also hook the execute function to track the current prompt_id
|
||||||
original_execute = execution.execute
|
original_execute = execution.execute
|
||||||
|
|
||||||
|
|||||||
@@ -181,13 +181,30 @@ class AutomaticMetadataParser(RecipeMetadataParser):
|
|||||||
# First use Civitai resources if available (more reliable source)
|
# First use Civitai resources if available (more reliable source)
|
||||||
if metadata.get("civitai_resources"):
|
if metadata.get("civitai_resources"):
|
||||||
for resource in metadata.get("civitai_resources", []):
|
for resource in metadata.get("civitai_resources", []):
|
||||||
|
# --- Added: Parse 'air' field if present ---
|
||||||
|
air = resource.get("air")
|
||||||
|
if air:
|
||||||
|
# Format: urn:air:sdxl:lora:civitai:1221007@1375651
|
||||||
|
# Or: urn:air:sdxl:checkpoint:civitai:623891@2019115
|
||||||
|
air_pattern = r"urn:air:[^:]+:(?P<type>[^:]+):civitai:(?P<modelId>\d+)@(?P<modelVersionId>\d+)"
|
||||||
|
air_match = re.match(air_pattern, air)
|
||||||
|
if air_match:
|
||||||
|
air_type = air_match.group("type")
|
||||||
|
air_modelId = int(air_match.group("modelId"))
|
||||||
|
air_modelVersionId = int(air_match.group("modelVersionId"))
|
||||||
|
# checkpoint/lycoris/lora/hypernet
|
||||||
|
resource["type"] = air_type
|
||||||
|
resource["modelId"] = air_modelId
|
||||||
|
resource["modelVersionId"] = air_modelVersionId
|
||||||
|
# --- End added ---
|
||||||
|
|
||||||
if resource.get("type") in ["lora", "lycoris", "hypernet"] and resource.get("modelVersionId"):
|
if resource.get("type") in ["lora", "lycoris", "hypernet"] and resource.get("modelVersionId"):
|
||||||
# Initialize lora entry
|
# Initialize lora entry
|
||||||
lora_entry = {
|
lora_entry = {
|
||||||
'id': resource.get("modelVersionId", 0),
|
'id': resource.get("modelVersionId", 0),
|
||||||
'modelId': resource.get("modelId", 0),
|
'modelId': resource.get("modelId", 0),
|
||||||
'name': resource.get("modelName", "Unknown LoRA"),
|
'name': resource.get("modelName", "Unknown LoRA"),
|
||||||
'version': resource.get("modelVersionName", ""),
|
'version': resource.get("modelVersionName", resource.get("versionName", "")),
|
||||||
'type': resource.get("type", "lora"),
|
'type': resource.get("type", "lora"),
|
||||||
'weight': round(float(resource.get("weight", 1.0)), 2),
|
'weight': round(float(resource.get("weight", 1.0)), 2),
|
||||||
'existsLocally': False,
|
'existsLocally': False,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class SettingsManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.settings_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'settings.json')
|
self.settings_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'settings.json')
|
||||||
self.settings = self._load_settings()
|
self.settings = self._load_settings()
|
||||||
|
self._auto_set_default_roots()
|
||||||
self._check_environment_variables()
|
self._check_environment_variables()
|
||||||
|
|
||||||
def _load_settings(self) -> Dict[str, Any]:
|
def _load_settings(self) -> Dict[str, Any]:
|
||||||
@@ -21,6 +22,28 @@ class SettingsManager:
|
|||||||
logger.error(f"Error loading settings: {e}")
|
logger.error(f"Error loading settings: {e}")
|
||||||
return self._get_default_settings()
|
return self._get_default_settings()
|
||||||
|
|
||||||
|
def _auto_set_default_roots(self):
|
||||||
|
"""Auto set default root paths if only one folder is present and default is empty."""
|
||||||
|
folder_paths = self.settings.get('folder_paths', {})
|
||||||
|
updated = False
|
||||||
|
# loras
|
||||||
|
loras = folder_paths.get('loras', [])
|
||||||
|
if isinstance(loras, list) and len(loras) == 1 and not self.settings.get('default_lora_root'):
|
||||||
|
self.settings['default_lora_root'] = loras[0]
|
||||||
|
updated = True
|
||||||
|
# checkpoints
|
||||||
|
checkpoints = folder_paths.get('checkpoints', [])
|
||||||
|
if isinstance(checkpoints, list) and len(checkpoints) == 1 and not self.settings.get('default_checkpoint_root'):
|
||||||
|
self.settings['default_checkpoint_root'] = checkpoints[0]
|
||||||
|
updated = True
|
||||||
|
# embeddings
|
||||||
|
embeddings = folder_paths.get('embeddings', [])
|
||||||
|
if isinstance(embeddings, list) and len(embeddings) == 1 and not self.settings.get('default_embedding_root'):
|
||||||
|
self.settings['default_embedding_root'] = embeddings[0]
|
||||||
|
updated = True
|
||||||
|
if updated:
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
def _check_environment_variables(self) -> None:
|
def _check_environment_variables(self) -> None:
|
||||||
"""Check for environment variables and update settings if needed"""
|
"""Check for environment variables and update settings if needed"""
|
||||||
env_api_key = os.environ.get('CIVITAI_API_KEY')
|
env_api_key = os.environ.get('CIVITAI_API_KEY')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "0.8.23"
|
version = "0.8.24"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -4,39 +4,11 @@ import {
|
|||||||
LORA_PATTERN,
|
LORA_PATTERN,
|
||||||
collectActiveLorasFromChain,
|
collectActiveLorasFromChain,
|
||||||
updateConnectedTriggerWords,
|
updateConnectedTriggerWords,
|
||||||
chainCallback
|
chainCallback,
|
||||||
|
mergeLoras
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { addLorasWidget } from "./loras_widget.js";
|
import { addLorasWidget } from "./loras_widget.js";
|
||||||
|
|
||||||
function mergeLoras(lorasText, lorasArr) {
|
|
||||||
const result = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
// Reset pattern index before using
|
|
||||||
LORA_PATTERN.lastIndex = 0;
|
|
||||||
|
|
||||||
// Parse text input and create initial entries
|
|
||||||
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
|
||||||
const name = match[1];
|
|
||||||
const modelStrength = Number(match[2]);
|
|
||||||
// Extract clip strength if provided, otherwise use model strength
|
|
||||||
const clipStrength = match[3] ? Number(match[3]) : modelStrength;
|
|
||||||
|
|
||||||
// Find if this lora exists in the array data
|
|
||||||
const existingLora = lorasArr.find(l => l.name === name);
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
name: name,
|
|
||||||
// Use existing strength if available, otherwise use input strength
|
|
||||||
strength: existingLora ? existingLora.strength : modelStrength,
|
|
||||||
active: existingLora ? existingLora.active : true,
|
|
||||||
clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LoraManager.LoraLoader",
|
name: "LoraManager.LoraLoader",
|
||||||
|
|
||||||
|
|||||||
@@ -4,39 +4,11 @@ import {
|
|||||||
getActiveLorasFromNode,
|
getActiveLorasFromNode,
|
||||||
collectActiveLorasFromChain,
|
collectActiveLorasFromChain,
|
||||||
updateConnectedTriggerWords,
|
updateConnectedTriggerWords,
|
||||||
chainCallback
|
chainCallback,
|
||||||
|
mergeLoras
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { addLorasWidget } from "./loras_widget.js";
|
import { addLorasWidget } from "./loras_widget.js";
|
||||||
|
|
||||||
function mergeLoras(lorasText, lorasArr) {
|
|
||||||
const result = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
// Reset pattern index before using
|
|
||||||
LORA_PATTERN.lastIndex = 0;
|
|
||||||
|
|
||||||
// Parse text input and create initial entries
|
|
||||||
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
|
||||||
const name = match[1];
|
|
||||||
const modelStrength = Number(match[2]);
|
|
||||||
// Extract clip strength if provided, otherwise use model strength
|
|
||||||
const clipStrength = match[3] ? Number(match[3]) : modelStrength;
|
|
||||||
|
|
||||||
// Find if this lora exists in the array data
|
|
||||||
const existingLora = lorasArr.find(l => l.name === name);
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
name: name,
|
|
||||||
// Use existing strength if available, otherwise use input strength
|
|
||||||
strength: existingLora ? existingLora.strength : modelStrength,
|
|
||||||
active: existingLora ? existingLora.active : true,
|
|
||||||
clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LoraManager.LoraStacker",
|
name: "LoraManager.LoraStacker",
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection } from "./loras_widget_components.js";
|
||||||
import { createToggle, createArrowButton, PreviewTooltip } from "./loras_widget_components.js";
|
|
||||||
import {
|
import {
|
||||||
parseLoraValue,
|
parseLoraValue,
|
||||||
formatLoraValue,
|
formatLoraValue,
|
||||||
@@ -11,7 +10,7 @@ import {
|
|||||||
CONTAINER_PADDING,
|
CONTAINER_PADDING,
|
||||||
EMPTY_CONTAINER_HEIGHT
|
EMPTY_CONTAINER_HEIGHT
|
||||||
} from "./loras_widget_utils.js";
|
} from "./loras_widget_utils.js";
|
||||||
import { initDrag, createContextMenu, initHeaderDrag } from "./loras_widget_events.js";
|
import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js";
|
||||||
|
|
||||||
export function addLorasWidget(node, name, opts, callback) {
|
export function addLorasWidget(node, name, opts, callback) {
|
||||||
// Create container for loras
|
// Create container for loras
|
||||||
@@ -42,6 +41,30 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
// Create preview tooltip instance
|
// Create preview tooltip instance
|
||||||
const previewTooltip = new PreviewTooltip();
|
const previewTooltip = new PreviewTooltip();
|
||||||
|
|
||||||
|
// Selection state - only one LoRA can be selected at a time
|
||||||
|
let selectedLora = null;
|
||||||
|
|
||||||
|
// Function to select a LoRA
|
||||||
|
const selectLora = (loraName) => {
|
||||||
|
selectedLora = loraName;
|
||||||
|
// Update visual feedback for all entries
|
||||||
|
container.querySelectorAll('.comfy-lora-entry').forEach(entry => {
|
||||||
|
const entryLoraName = entry.dataset.loraName;
|
||||||
|
updateEntrySelection(entry, entryLoraName === selectedLora);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add keyboard event listener to container
|
||||||
|
container.addEventListener('keydown', (e) => {
|
||||||
|
if (handleKeyboardNavigation(e, selectedLora, widget, renderLoras, selectLora)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make container focusable for keyboard events
|
||||||
|
container.tabIndex = 0;
|
||||||
|
container.style.outline = 'none';
|
||||||
|
|
||||||
// Function to render loras from data
|
// Function to render loras from data
|
||||||
const renderLoras = (value, widget) => {
|
const renderLoras = (value, widget) => {
|
||||||
// Clear existing content
|
// Clear existing content
|
||||||
@@ -185,6 +208,26 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
marginBottom: "4px",
|
marginBottom: "4px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store lora name and active state in dataset for selection
|
||||||
|
loraEl.dataset.loraName = name;
|
||||||
|
loraEl.dataset.active = active;
|
||||||
|
|
||||||
|
// Add click handler for selection
|
||||||
|
loraEl.addEventListener('click', (e) => {
|
||||||
|
// Skip if clicking on interactive elements
|
||||||
|
if (e.target.closest('.comfy-lora-toggle') ||
|
||||||
|
e.target.closest('input') ||
|
||||||
|
e.target.closest('.comfy-lora-arrow') ||
|
||||||
|
e.target.closest('.comfy-lora-drag-handle')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
selectLora(name);
|
||||||
|
container.focus(); // Focus container for keyboard events
|
||||||
|
});
|
||||||
|
|
||||||
// Add double-click handler to toggle clip entry
|
// Add double-click handler to toggle clip entry
|
||||||
loraEl.addEventListener('dblclick', (e) => {
|
loraEl.addEventListener('dblclick', (e) => {
|
||||||
// Skip if clicking on toggle or strength control areas
|
// Skip if clicking on toggle or strength control areas
|
||||||
@@ -220,6 +263,12 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create drag handle for reordering
|
||||||
|
const dragHandle = createDragHandle();
|
||||||
|
|
||||||
|
// Initialize reorder drag functionality
|
||||||
|
initReorderDrag(dragHandle, name, widget, renderLoras);
|
||||||
|
|
||||||
// Create toggle for this lora
|
// Create toggle for this lora
|
||||||
const toggle = createToggle(active, (newActive) => {
|
const toggle = createToggle(active, (newActive) => {
|
||||||
// Update this lora's active state
|
// Update this lora's active state
|
||||||
@@ -416,6 +465,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
minWidth: "0", // Allow shrinking
|
minWidth: "0", // Allow shrinking
|
||||||
});
|
});
|
||||||
|
|
||||||
|
leftSection.appendChild(dragHandle); // Add drag handle first
|
||||||
leftSection.appendChild(toggle);
|
leftSection.appendChild(toggle);
|
||||||
leftSection.appendChild(nameEl);
|
leftSection.appendChild(nameEl);
|
||||||
|
|
||||||
@@ -424,6 +474,9 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
|
|
||||||
container.appendChild(loraEl);
|
container.appendChild(loraEl);
|
||||||
|
|
||||||
|
// Update selection state
|
||||||
|
updateEntrySelection(loraEl, name === selectedLora);
|
||||||
|
|
||||||
// If expanded, show the clip entry
|
// If expanded, show the clip entry
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
totalVisibleEntries++;
|
totalVisibleEntries++;
|
||||||
@@ -444,6 +497,10 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
marginTop: "-2px"
|
marginTop: "-2px"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store the same lora name in clip entry dataset
|
||||||
|
clipEl.dataset.loraName = name;
|
||||||
|
clipEl.dataset.active = active;
|
||||||
|
|
||||||
// Create clip name display
|
// Create clip name display
|
||||||
const clipNameEl = document.createElement("div");
|
const clipNameEl = document.createElement("div");
|
||||||
clipNameEl.textContent = "[clip] " + name;
|
clipNameEl.textContent = "[clip] " + name;
|
||||||
@@ -601,7 +658,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Calculate height based on number of loras and fixed sizes
|
// Calculate height based on number of loras and fixed sizes
|
||||||
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 10) * LORA_ENTRY_HEIGHT);
|
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 12) * LORA_ENTRY_HEIGHT);
|
||||||
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
|
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -685,6 +742,8 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
widget.onRemove = () => {
|
widget.onRemove = () => {
|
||||||
container.remove();
|
container.remove();
|
||||||
previewTooltip.cleanup();
|
previewTooltip.cleanup();
|
||||||
|
// Remove keyboard event listener
|
||||||
|
container.removeEventListener('keydown', handleKeyboardNavigation);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { minWidth: 400, minHeight: defaultHeight, widget };
|
return { minWidth: 400, minHeight: defaultHeight, widget };
|
||||||
|
|||||||
@@ -78,6 +78,87 @@ export function createArrowButton(direction, onClick) {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to create drag handle
|
||||||
|
export function createDragHandle() {
|
||||||
|
const handle = document.createElement("div");
|
||||||
|
handle.className = "comfy-lora-drag-handle";
|
||||||
|
handle.innerHTML = "≡";
|
||||||
|
handle.title = "Drag to reorder LoRA";
|
||||||
|
|
||||||
|
Object.assign(handle.style, {
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "rgba(226, 232, 240, 0.6)",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
marginRight: "8px",
|
||||||
|
flexShrink: "0"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add hover effect
|
||||||
|
handle.onmouseenter = () => {
|
||||||
|
handle.style.color = "rgba(226, 232, 240, 0.9)";
|
||||||
|
handle.style.transform = "scale(1.1)";
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.onmouseleave = () => {
|
||||||
|
handle.style.color = "rgba(226, 232, 240, 0.6)";
|
||||||
|
handle.style.transform = "scale(1)";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change cursor when dragging
|
||||||
|
handle.onmousedown = () => {
|
||||||
|
handle.style.cursor = "grabbing";
|
||||||
|
};
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create drop indicator
|
||||||
|
export function createDropIndicator() {
|
||||||
|
const indicator = document.createElement("div");
|
||||||
|
indicator.className = "comfy-lora-drop-indicator";
|
||||||
|
|
||||||
|
Object.assign(indicator.style, {
|
||||||
|
position: "absolute",
|
||||||
|
left: "0",
|
||||||
|
right: "0",
|
||||||
|
height: "3px",
|
||||||
|
backgroundColor: "rgba(66, 153, 225, 0.9)",
|
||||||
|
borderRadius: "2px",
|
||||||
|
opacity: "0",
|
||||||
|
transition: "opacity 0.2s ease",
|
||||||
|
boxShadow: "0 0 6px rgba(66, 153, 225, 0.8)",
|
||||||
|
zIndex: "10",
|
||||||
|
pointerEvents: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
return indicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update entry selection state
|
||||||
|
export function updateEntrySelection(entryEl, isSelected) {
|
||||||
|
const baseColor = entryEl.dataset.active === 'true' ?
|
||||||
|
"rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)";
|
||||||
|
const selectedColor = entryEl.dataset.active === 'true' ?
|
||||||
|
"rgba(66, 153, 225, 0.3)" : "rgba(66, 153, 225, 0.2)";
|
||||||
|
|
||||||
|
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)";
|
||||||
|
} else {
|
||||||
|
entryEl.style.backgroundColor = baseColor;
|
||||||
|
entryEl.style.border = "1px solid transparent";
|
||||||
|
entryEl.style.boxShadow = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to create menu item
|
// Function to create menu item
|
||||||
export function createMenuItem(text, icon, onClick) {
|
export function createMenuItem(text, icon, onClick) {
|
||||||
const menuItem = document.createElement('div');
|
const menuItem = document.createElement('div');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { createMenuItem } from "./loras_widget_components.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast } from "./loras_widget_utils.js";
|
import { createMenuItem, createDropIndicator } from "./loras_widget_components.js";
|
||||||
|
import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast, moveLoraByDirection, getDropTargetIndex } from "./loras_widget_utils.js";
|
||||||
|
|
||||||
// Function to handle strength adjustment via dragging
|
// Function to handle strength adjustment via dragging
|
||||||
export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) {
|
export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) {
|
||||||
@@ -227,6 +228,223 @@ export function initHeaderDrag(headerEl, widget, renderFunction) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to initialize drag-and-drop for reordering
|
||||||
|
export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
|
||||||
|
let isDragging = false;
|
||||||
|
let draggedElement = null;
|
||||||
|
let dropIndicator = null;
|
||||||
|
let container = null;
|
||||||
|
let scale = 1;
|
||||||
|
|
||||||
|
dragHandle.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
draggedElement = dragHandle.closest('.comfy-lora-entry');
|
||||||
|
container = draggedElement.parentElement;
|
||||||
|
|
||||||
|
// Add dragging class and visual feedback
|
||||||
|
draggedElement.classList.add('comfy-lora-dragging');
|
||||||
|
draggedElement.style.opacity = '0.5';
|
||||||
|
draggedElement.style.transform = 'scale(0.98)';
|
||||||
|
|
||||||
|
// Create single drop indicator with absolute positioning
|
||||||
|
dropIndicator = createDropIndicator();
|
||||||
|
|
||||||
|
// Make container relatively positioned for absolute indicator
|
||||||
|
const originalPosition = container.style.position;
|
||||||
|
container.style.position = 'relative';
|
||||||
|
container.appendChild(dropIndicator);
|
||||||
|
|
||||||
|
// Store original position for cleanup
|
||||||
|
container._originalPosition = originalPosition;
|
||||||
|
|
||||||
|
// Add global cursor style
|
||||||
|
document.body.style.cursor = 'grabbing';
|
||||||
|
|
||||||
|
// Store workflow scale for accurate positioning
|
||||||
|
scale = app.canvas.ds.scale;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging || !draggedElement || !dropIndicator) return;
|
||||||
|
|
||||||
|
const targetIndex = getDropTargetIndex(container, e.clientY);
|
||||||
|
const entries = container.querySelectorAll('.comfy-lora-entry, .comfy-lora-clip-entry');
|
||||||
|
|
||||||
|
if (targetIndex === 0) {
|
||||||
|
// Show at top
|
||||||
|
const firstEntry = entries[0];
|
||||||
|
if (firstEntry) {
|
||||||
|
const rect = firstEntry.getBoundingClientRect();
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
dropIndicator.style.top = `${(rect.top - containerRect.top - 2) / scale}px`;
|
||||||
|
dropIndicator.style.opacity = '1';
|
||||||
|
}
|
||||||
|
} else if (targetIndex < entries.length) {
|
||||||
|
// Show between entries
|
||||||
|
const targetEntry = entries[targetIndex];
|
||||||
|
if (targetEntry) {
|
||||||
|
const rect = targetEntry.getBoundingClientRect();
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
dropIndicator.style.top = `${(rect.top - containerRect.top - 2) / scale}px`;
|
||||||
|
dropIndicator.style.opacity = '1';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show at bottom
|
||||||
|
const lastEntry = entries[entries.length - 1];
|
||||||
|
if (lastEntry) {
|
||||||
|
const rect = lastEntry.getBoundingClientRect();
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
dropIndicator.style.top = `${(rect.bottom - containerRect.top + 2) / scale}px`;
|
||||||
|
dropIndicator.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', (e) => {
|
||||||
|
if (!isDragging || !draggedElement) return;
|
||||||
|
|
||||||
|
const targetIndex = getDropTargetIndex(container, e.clientY);
|
||||||
|
|
||||||
|
// Get current LoRA data
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const currentIndex = lorasData.findIndex(l => l.name === loraName);
|
||||||
|
|
||||||
|
if (currentIndex !== -1 && currentIndex !== targetIndex) {
|
||||||
|
// Calculate actual target index (excluding clip entries from count)
|
||||||
|
const loraEntries = container.querySelectorAll('.comfy-lora-entry');
|
||||||
|
let actualTargetIndex = targetIndex;
|
||||||
|
|
||||||
|
// Adjust target index if it's beyond the number of actual LoRA entries
|
||||||
|
if (actualTargetIndex > loraEntries.length) {
|
||||||
|
actualTargetIndex = loraEntries.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the LoRA
|
||||||
|
const newLoras = [...lorasData];
|
||||||
|
const [moved] = newLoras.splice(currentIndex, 1);
|
||||||
|
newLoras.splice(actualTargetIndex > currentIndex ? actualTargetIndex - 1 : actualTargetIndex, 0, moved);
|
||||||
|
|
||||||
|
widget.value = formatLoraValue(newLoras);
|
||||||
|
|
||||||
|
if (widget.callback) {
|
||||||
|
widget.callback(widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
if (renderFunction) {
|
||||||
|
renderFunction(widget.value, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
isDragging = false;
|
||||||
|
if (draggedElement) {
|
||||||
|
draggedElement.classList.remove('comfy-lora-dragging');
|
||||||
|
draggedElement.style.opacity = '';
|
||||||
|
draggedElement.style.transform = '';
|
||||||
|
draggedElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropIndicator && container) {
|
||||||
|
container.removeChild(dropIndicator);
|
||||||
|
// Restore original position
|
||||||
|
container.style.position = container._originalPosition || '';
|
||||||
|
delete container._originalPosition;
|
||||||
|
dropIndicator = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset cursor
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle keyboard navigation
|
||||||
|
export function handleKeyboardNavigation(event, selectedLora, widget, renderFunction, selectLora) {
|
||||||
|
if (!selectedLora) return false;
|
||||||
|
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
// Check for Ctrl/Cmd modifier for reordering
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
const newLorasUp = moveLoraByDirection(lorasData, selectedLora, 'up');
|
||||||
|
widget.value = formatLoraValue(newLorasUp);
|
||||||
|
if (widget.callback) widget.callback(widget.value);
|
||||||
|
if (renderFunction) renderFunction(widget.value, widget);
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
const newLorasDown = moveLoraByDirection(lorasData, selectedLora, 'down');
|
||||||
|
widget.value = formatLoraValue(newLorasDown);
|
||||||
|
if (widget.callback) widget.callback(widget.value);
|
||||||
|
if (renderFunction) renderFunction(widget.value, widget);
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Home':
|
||||||
|
event.preventDefault();
|
||||||
|
const newLorasTop = moveLoraByDirection(lorasData, selectedLora, 'top');
|
||||||
|
widget.value = formatLoraValue(newLorasTop);
|
||||||
|
if (widget.callback) widget.callback(widget.value);
|
||||||
|
if (renderFunction) renderFunction(widget.value, widget);
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'End':
|
||||||
|
event.preventDefault();
|
||||||
|
const newLorasBottom = moveLoraByDirection(lorasData, selectedLora, 'bottom');
|
||||||
|
widget.value = formatLoraValue(newLorasBottom);
|
||||||
|
if (widget.callback) widget.callback(widget.value);
|
||||||
|
if (renderFunction) renderFunction(widget.value, widget);
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal navigation without Ctrl/Cmd
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
const currentIndex = lorasData.findIndex(l => l.name === selectedLora);
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
selectLora(lorasData[currentIndex - 1].name);
|
||||||
|
}
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
const currentIndexDown = lorasData.findIndex(l => l.name === selectedLora);
|
||||||
|
if (currentIndexDown < lorasData.length - 1) {
|
||||||
|
selectLora(lorasData[currentIndexDown + 1].name);
|
||||||
|
}
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Delete':
|
||||||
|
case 'Backspace':
|
||||||
|
event.preventDefault();
|
||||||
|
const filtered = lorasData.filter(l => l.name !== selectedLora);
|
||||||
|
widget.value = formatLoraValue(filtered);
|
||||||
|
if (widget.callback) widget.callback(widget.value);
|
||||||
|
if (renderFunction) renderFunction(widget.value, widget);
|
||||||
|
selectLora(null); // Clear selection
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to create context menu
|
// Function to create context menu
|
||||||
export function createContextMenu(x, y, loraName, widget, previewTooltip, renderFunction) {
|
export function createContextMenu(x, y, loraName, widget, previewTooltip, renderFunction) {
|
||||||
// Hide preview tooltip first
|
// Hide preview tooltip first
|
||||||
@@ -398,6 +616,94 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Move Up option with arrow up icon
|
||||||
|
const moveUpOption = createMenuItem(
|
||||||
|
'Move Up',
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"></path></svg>',
|
||||||
|
() => {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const newLoras = moveLoraByDirection(lorasData, loraName, 'up');
|
||||||
|
widget.value = formatLoraValue(newLoras);
|
||||||
|
|
||||||
|
if (widget.callback) {
|
||||||
|
widget.callback(widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderFunction) {
|
||||||
|
renderFunction(widget.value, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move Down option with arrow down icon
|
||||||
|
const moveDownOption = createMenuItem(
|
||||||
|
'Move Down',
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"></path></svg>',
|
||||||
|
() => {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const newLoras = moveLoraByDirection(lorasData, loraName, 'down');
|
||||||
|
widget.value = formatLoraValue(newLoras);
|
||||||
|
|
||||||
|
if (widget.callback) {
|
||||||
|
widget.callback(widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderFunction) {
|
||||||
|
renderFunction(widget.value, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move to Top option with chevrons up icon
|
||||||
|
const moveTopOption = createMenuItem(
|
||||||
|
'Move to Top',
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 11l-5-5-5 5M17 18l-5-5-5 5"></path></svg>',
|
||||||
|
() => {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const newLoras = moveLoraByDirection(lorasData, loraName, 'top');
|
||||||
|
widget.value = formatLoraValue(newLoras);
|
||||||
|
|
||||||
|
if (widget.callback) {
|
||||||
|
widget.callback(widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderFunction) {
|
||||||
|
renderFunction(widget.value, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move to Bottom option with chevrons down icon
|
||||||
|
const moveBottomOption = createMenuItem(
|
||||||
|
'Move to Bottom',
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 13l5 5 5-5M7 6l5 5 5-5"></path></svg>',
|
||||||
|
() => {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const newLoras = moveLoraByDirection(lorasData, loraName, 'bottom');
|
||||||
|
widget.value = formatLoraValue(newLoras);
|
||||||
|
|
||||||
|
if (widget.callback) {
|
||||||
|
widget.callback(widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderFunction) {
|
||||||
|
renderFunction(widget.value, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Add separator
|
// Add separator
|
||||||
const separator1 = document.createElement('div');
|
const separator1 = document.createElement('div');
|
||||||
Object.assign(separator1.style, {
|
Object.assign(separator1.style, {
|
||||||
@@ -412,9 +718,21 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
|
|||||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add separator for order options
|
||||||
|
const orderSeparator = document.createElement('div');
|
||||||
|
Object.assign(orderSeparator.style, {
|
||||||
|
margin: '4px 0',
|
||||||
|
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
});
|
||||||
|
|
||||||
menu.appendChild(viewOnCivitaiOption);
|
menu.appendChild(viewOnCivitaiOption);
|
||||||
menu.appendChild(deleteOption);
|
menu.appendChild(deleteOption);
|
||||||
menu.appendChild(separator1);
|
menu.appendChild(separator1);
|
||||||
|
menu.appendChild(moveUpOption);
|
||||||
|
menu.appendChild(moveDownOption);
|
||||||
|
menu.appendChild(moveTopOption);
|
||||||
|
menu.appendChild(moveBottomOption);
|
||||||
|
menu.appendChild(orderSeparator);
|
||||||
menu.appendChild(copyNotesOption);
|
menu.appendChild(copyNotesOption);
|
||||||
menu.appendChild(copyTriggerWordsOption);
|
menu.appendChild(copyTriggerWordsOption);
|
||||||
menu.appendChild(separator2);
|
menu.appendChild(separator2);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { app } from "../../scripts/app.js";
|
|||||||
// Fixed sizes for component calculations
|
// Fixed sizes for component calculations
|
||||||
export const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry
|
export const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry
|
||||||
export const CLIP_ENTRY_HEIGHT = 40; // Height of a clip entry
|
export const CLIP_ENTRY_HEIGHT = 40; // Height of a clip entry
|
||||||
export const HEADER_HEIGHT = 40; // Height of the header section
|
export const HEADER_HEIGHT = 32; // Height of the header section
|
||||||
export const CONTAINER_PADDING = 12; // Top and bottom padding
|
export const CONTAINER_PADDING = 12; // Top and bottom padding
|
||||||
export const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
|
export const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
|
||||||
|
|
||||||
@@ -164,3 +164,71 @@ export function showToast(message, type = 'info') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a LoRA to a new position in the array
|
||||||
|
* @param {Array} loras - Array of LoRA objects
|
||||||
|
* @param {number} fromIndex - Current index of the LoRA
|
||||||
|
* @param {number} toIndex - Target index for the LoRA
|
||||||
|
* @returns {Array} - New array with LoRA moved
|
||||||
|
*/
|
||||||
|
export function moveLoraInArray(loras, fromIndex, toIndex) {
|
||||||
|
const newLoras = [...loras];
|
||||||
|
const [removed] = newLoras.splice(fromIndex, 1);
|
||||||
|
newLoras.splice(toIndex, 0, removed);
|
||||||
|
return newLoras;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a LoRA by name to a specific position
|
||||||
|
* @param {Array} loras - Array of LoRA objects
|
||||||
|
* @param {string} loraName - Name of the LoRA to move
|
||||||
|
* @param {string} direction - 'up', 'down', 'top', 'bottom'
|
||||||
|
* @returns {Array} - New array with LoRA moved
|
||||||
|
*/
|
||||||
|
export function moveLoraByDirection(loras, loraName, direction) {
|
||||||
|
const currentIndex = loras.findIndex(l => l.name === loraName);
|
||||||
|
if (currentIndex === -1) return loras;
|
||||||
|
|
||||||
|
let newIndex;
|
||||||
|
switch (direction) {
|
||||||
|
case 'up':
|
||||||
|
newIndex = Math.max(0, currentIndex - 1);
|
||||||
|
break;
|
||||||
|
case 'down':
|
||||||
|
newIndex = Math.min(loras.length - 1, currentIndex + 1);
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
newIndex = 0;
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
newIndex = loras.length - 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return loras;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIndex === currentIndex) return loras;
|
||||||
|
return moveLoraInArray(loras, currentIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the drop target index based on mouse position
|
||||||
|
* @param {HTMLElement} container - The container element
|
||||||
|
* @param {number} clientY - Mouse Y position
|
||||||
|
* @returns {number} - Target index for dropping
|
||||||
|
*/
|
||||||
|
export function getDropTargetIndex(container, clientY) {
|
||||||
|
const entries = container.querySelectorAll('.comfy-lora-entry');
|
||||||
|
let targetIndex = entries.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const rect = entries[i].getBoundingClientRect();
|
||||||
|
if (clientY < rect.top + rect.height / 2) {
|
||||||
|
targetIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetIndex;
|
||||||
|
}
|
||||||
|
|||||||
@@ -183,4 +183,47 @@ export function updateConnectedTriggerWords(node, loraNames) {
|
|||||||
})
|
})
|
||||||
}).catch(err => console.error("Error fetching trigger words:", err));
|
}).catch(err => console.error("Error fetching trigger words:", err));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeLoras(lorasText, lorasArr) {
|
||||||
|
// Parse lorasText into a map: name -> {strength, clipStrength}
|
||||||
|
const parsedLoras = {};
|
||||||
|
let match;
|
||||||
|
LORA_PATTERN.lastIndex = 0;
|
||||||
|
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
||||||
|
const name = match[1];
|
||||||
|
const modelStrength = Number(match[2]);
|
||||||
|
const clipStrength = match[3] ? Number(match[3]) : modelStrength;
|
||||||
|
parsedLoras[name] = { strength: modelStrength, clipStrength };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result array in the order of lorasArr
|
||||||
|
const result = [];
|
||||||
|
const usedNames = new Set();
|
||||||
|
|
||||||
|
for (const lora of lorasArr) {
|
||||||
|
if (parsedLoras[lora.name]) {
|
||||||
|
result.push({
|
||||||
|
name: lora.name,
|
||||||
|
strength: lora.strength !== undefined ? lora.strength : parsedLoras[lora.name].strength,
|
||||||
|
active: lora.active !== undefined ? lora.active : true,
|
||||||
|
clipStrength: lora.clipStrength !== undefined ? lora.clipStrength : parsedLoras[lora.name].clipStrength,
|
||||||
|
});
|
||||||
|
usedNames.add(lora.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any new loras from lorasText that are not in lorasArr, in their text order
|
||||||
|
for (const name in parsedLoras) {
|
||||||
|
if (!usedNames.has(name)) {
|
||||||
|
result.push({
|
||||||
|
name,
|
||||||
|
strength: parsedLoras[name].strength,
|
||||||
|
active: true,
|
||||||
|
clipStrength: parsedLoras[name].clipStrength,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
@@ -2,41 +2,12 @@ import { app } from "../../scripts/app.js";
|
|||||||
import {
|
import {
|
||||||
LORA_PATTERN,
|
LORA_PATTERN,
|
||||||
getActiveLorasFromNode,
|
getActiveLorasFromNode,
|
||||||
collectActiveLorasFromChain,
|
|
||||||
updateConnectedTriggerWords,
|
updateConnectedTriggerWords,
|
||||||
chainCallback
|
chainCallback,
|
||||||
|
mergeLoras
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { addLorasWidget } from "./loras_widget.js";
|
import { addLorasWidget } from "./loras_widget.js";
|
||||||
|
|
||||||
function mergeLoras(lorasText, lorasArr) {
|
|
||||||
const result = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
// Reset pattern index before using
|
|
||||||
LORA_PATTERN.lastIndex = 0;
|
|
||||||
|
|
||||||
// Parse text input and create initial entries
|
|
||||||
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
|
||||||
const name = match[1];
|
|
||||||
const modelStrength = Number(match[2]);
|
|
||||||
// Extract clip strength if provided, otherwise use model strength
|
|
||||||
const clipStrength = match[3] ? Number(match[3]) : modelStrength;
|
|
||||||
|
|
||||||
// Find if this lora exists in the array data
|
|
||||||
const existingLora = lorasArr.find(l => l.name === name);
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
name: name,
|
|
||||||
// Use existing strength if available, otherwise use input strength
|
|
||||||
strength: existingLora ? existingLora.strength : modelStrength,
|
|
||||||
active: existingLora ? existingLora.active : true,
|
|
||||||
clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LoraManager.WanVideoLoraSelect",
|
name: "LoraManager.WanVideoLoraSelect",
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user