mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Restructure the code of loras_widget into smaller, more manageable modules.
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { createToggle, createArrowButton, PreviewTooltip } from "./loras_widget_components.js";
|
||||
import {
|
||||
parseLoraValue,
|
||||
formatLoraValue,
|
||||
updateWidgetHeight,
|
||||
shouldShowClipEntry,
|
||||
syncClipStrengthIfCollapsed,
|
||||
LORA_ENTRY_HEIGHT,
|
||||
HEADER_HEIGHT,
|
||||
CONTAINER_PADDING,
|
||||
EMPTY_CONTAINER_HEIGHT
|
||||
} from "./loras_widget_utils.js";
|
||||
import { initDrag, createContextMenu } from "./loras_widget_events.js";
|
||||
|
||||
export function addLorasWidget(node, name, opts, callback) {
|
||||
// Create container for loras
|
||||
@@ -27,586 +39,9 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
// Initialize default value
|
||||
const defaultValue = opts?.defaultVal || [];
|
||||
|
||||
// Fixed sizes for component calculations
|
||||
const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry
|
||||
const CLIP_ENTRY_HEIGHT = 40; // Height of a clip entry
|
||||
const HEADER_HEIGHT = 40; // Height of the header section
|
||||
const CONTAINER_PADDING = 12; // Top and bottom padding
|
||||
const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
|
||||
|
||||
// Remove expandedClipEntries Set since we'll determine expansion based on strength values
|
||||
|
||||
// Parse LoRA entries from value
|
||||
const parseLoraValue = (value) => {
|
||||
if (!value) return [];
|
||||
return Array.isArray(value) ? value : [];
|
||||
};
|
||||
|
||||
// Format LoRA data
|
||||
const formatLoraValue = (loras) => {
|
||||
return loras;
|
||||
};
|
||||
|
||||
// Function to update widget height consistently
|
||||
const updateWidgetHeight = (height) => {
|
||||
// Ensure minimum height
|
||||
const finalHeight = Math.max(defaultHeight, height);
|
||||
|
||||
// Update CSS variables
|
||||
container.style.setProperty('--comfy-widget-min-height', `${finalHeight}px`);
|
||||
container.style.setProperty('--comfy-widget-height', `${finalHeight}px`);
|
||||
|
||||
// Force node to update size after a short delay to ensure DOM is updated
|
||||
if (node) {
|
||||
setTimeout(() => {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to create toggle element
|
||||
const createToggle = (active, onChange) => {
|
||||
const toggle = document.createElement("div");
|
||||
toggle.className = "comfy-lora-toggle";
|
||||
|
||||
updateToggleStyle(toggle, active);
|
||||
|
||||
toggle.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
onChange(!active);
|
||||
});
|
||||
|
||||
return toggle;
|
||||
};
|
||||
|
||||
// Helper function to update toggle style
|
||||
function updateToggleStyle(toggleEl, active) {
|
||||
Object.assign(toggleEl.style, {
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
backgroundColor: active ? "rgba(66, 153, 225, 0.9)" : "rgba(45, 55, 72, 0.7)",
|
||||
border: `1px solid ${active ? "rgba(66, 153, 225, 0.9)" : "rgba(226, 232, 240, 0.2)"}`,
|
||||
});
|
||||
|
||||
// Add hover effect
|
||||
toggleEl.onmouseenter = () => {
|
||||
toggleEl.style.transform = "scale(1.05)";
|
||||
toggleEl.style.boxShadow = "0 2px 4px rgba(0,0,0,0.15)";
|
||||
};
|
||||
|
||||
toggleEl.onmouseleave = () => {
|
||||
toggleEl.style.transform = "scale(1)";
|
||||
toggleEl.style.boxShadow = "none";
|
||||
};
|
||||
}
|
||||
|
||||
// Create arrow button for strength adjustment
|
||||
const createArrowButton = (direction, onClick) => {
|
||||
const button = document.createElement("div");
|
||||
button.className = `comfy-lora-arrow comfy-lora-arrow-${direction}`;
|
||||
|
||||
Object.assign(button.style, {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
fontSize: "12px",
|
||||
color: "rgba(226, 232, 240, 0.8)",
|
||||
transition: "all 0.2s ease",
|
||||
});
|
||||
|
||||
button.textContent = direction === "left" ? "◀" : "▶";
|
||||
|
||||
button.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
});
|
||||
|
||||
// Add hover effect
|
||||
button.onmouseenter = () => {
|
||||
button.style.color = "white";
|
||||
button.style.transform = "scale(1.2)";
|
||||
};
|
||||
|
||||
button.onmouseleave = () => {
|
||||
button.style.color = "rgba(226, 232, 240, 0.8)";
|
||||
button.style.transform = "scale(1)";
|
||||
};
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
// Preview tooltip class
|
||||
class PreviewTooltip {
|
||||
constructor() {
|
||||
this.element = document.createElement('div');
|
||||
Object.assign(this.element.style, {
|
||||
position: 'fixed',
|
||||
zIndex: 9999,
|
||||
background: 'rgba(0, 0, 0, 0.85)',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
display: 'none',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '300px',
|
||||
});
|
||||
document.body.appendChild(this.element);
|
||||
this.hideTimeout = null;
|
||||
|
||||
// Add global click event to hide tooltip
|
||||
document.addEventListener('click', () => this.hide());
|
||||
|
||||
// Add scroll event listener
|
||||
document.addEventListener('scroll', () => this.hide(), true);
|
||||
}
|
||||
|
||||
async show(loraName, x, y) {
|
||||
try {
|
||||
// Clear previous hide timer
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = null;
|
||||
}
|
||||
|
||||
// Don't redisplay the same lora preview
|
||||
if (this.element.style.display === 'block' && this.currentLora === loraName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLora = loraName;
|
||||
|
||||
// Get preview URL
|
||||
const response = await api.fetchApi(`/lora-preview-url?name=${encodeURIComponent(loraName)}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch preview URL');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.preview_url) {
|
||||
throw new Error('No preview available');
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
while (this.element.firstChild) {
|
||||
this.element.removeChild(this.element.firstChild);
|
||||
}
|
||||
|
||||
// Create media container with relative positioning
|
||||
const mediaContainer = document.createElement('div');
|
||||
Object.assign(mediaContainer.style, {
|
||||
position: 'relative',
|
||||
maxWidth: '300px',
|
||||
maxHeight: '300px',
|
||||
});
|
||||
|
||||
const isVideo = data.preview_url.endsWith('.mp4');
|
||||
const mediaElement = isVideo ? document.createElement('video') : document.createElement('img');
|
||||
|
||||
Object.assign(mediaElement.style, {
|
||||
maxWidth: '300px',
|
||||
maxHeight: '300px',
|
||||
objectFit: 'contain',
|
||||
display: 'block',
|
||||
});
|
||||
|
||||
if (isVideo) {
|
||||
mediaElement.autoplay = true;
|
||||
mediaElement.loop = true;
|
||||
mediaElement.muted = true;
|
||||
mediaElement.controls = false;
|
||||
}
|
||||
|
||||
mediaElement.src = data.preview_url;
|
||||
|
||||
// Create name label with absolute positioning
|
||||
const nameLabel = document.createElement('div');
|
||||
nameLabel.textContent = loraName;
|
||||
Object.assign(nameLabel.style, {
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
left: '0',
|
||||
right: '0',
|
||||
padding: '8px',
|
||||
color: 'rgba(255, 255, 255, 0.95)',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif",
|
||||
background: 'linear-gradient(transparent, rgba(0, 0, 0, 0.8))',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textAlign: 'center',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
});
|
||||
|
||||
mediaContainer.appendChild(mediaElement);
|
||||
mediaContainer.appendChild(nameLabel);
|
||||
this.element.appendChild(mediaContainer);
|
||||
|
||||
// Add fade-in effect
|
||||
this.element.style.opacity = '0';
|
||||
this.element.style.display = 'block';
|
||||
this.position(x, y);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.element.style.transition = 'opacity 0.15s ease';
|
||||
this.element.style.opacity = '1';
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to load preview:', error);
|
||||
}
|
||||
}
|
||||
|
||||
position(x, y) {
|
||||
// Ensure preview box doesn't exceed viewport boundaries
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let left = x + 10; // Default 10px offset to the right of mouse
|
||||
let top = y + 10; // Default 10px offset below mouse
|
||||
|
||||
// Check right boundary
|
||||
if (left + rect.width > viewportWidth) {
|
||||
left = x - rect.width - 10;
|
||||
}
|
||||
|
||||
// Check bottom boundary
|
||||
if (top + rect.height > viewportHeight) {
|
||||
top = y - rect.height - 10;
|
||||
}
|
||||
|
||||
Object.assign(this.element.style, {
|
||||
left: `${left}px`,
|
||||
top: `${top}px`
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
// Use fade-out effect
|
||||
if (this.element.style.display === 'block') {
|
||||
this.element.style.opacity = '0';
|
||||
this.hideTimeout = setTimeout(() => {
|
||||
this.element.style.display = 'none';
|
||||
this.currentLora = null;
|
||||
// Stop video playback
|
||||
const video = this.element.querySelector('video');
|
||||
if (video) {
|
||||
video.pause();
|
||||
}
|
||||
this.hideTimeout = null;
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
}
|
||||
// Remove all event listeners
|
||||
document.removeEventListener('click', () => this.hide());
|
||||
document.removeEventListener('scroll', () => this.hide(), true);
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Create preview tooltip instance
|
||||
const previewTooltip = new PreviewTooltip();
|
||||
|
||||
// Function to create menu item
|
||||
const createMenuItem = (text, icon, onClick) => {
|
||||
const menuItem = document.createElement('div');
|
||||
Object.assign(menuItem.style, {
|
||||
padding: '6px 20px',
|
||||
cursor: 'pointer',
|
||||
color: 'rgba(226, 232, 240, 0.9)',
|
||||
fontSize: '13px',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
// Create icon element
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.innerHTML = icon;
|
||||
Object.assign(iconEl.style, {
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
// Create text element
|
||||
const textEl = document.createElement('span');
|
||||
textEl.textContent = text;
|
||||
|
||||
menuItem.appendChild(iconEl);
|
||||
menuItem.appendChild(textEl);
|
||||
|
||||
menuItem.addEventListener('mouseenter', () => {
|
||||
menuItem.style.backgroundColor = 'rgba(66, 153, 225, 0.2)';
|
||||
});
|
||||
|
||||
menuItem.addEventListener('mouseleave', () => {
|
||||
menuItem.style.backgroundColor = 'transparent';
|
||||
});
|
||||
|
||||
if (onClick) {
|
||||
menuItem.addEventListener('click', onClick);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
};
|
||||
|
||||
// Function to handle strength adjustment via dragging
|
||||
const handleStrengthDrag = (name, initialStrength, initialX, event, widget, isClipStrength = false) => {
|
||||
// Calculate drag sensitivity (how much the strength changes per pixel)
|
||||
// Using 0.01 per 10 pixels of movement
|
||||
const sensitivity = 0.001;
|
||||
|
||||
// Get the current mouse position
|
||||
const currentX = event.clientX;
|
||||
|
||||
// Calculate the distance moved
|
||||
const deltaX = currentX - initialX;
|
||||
|
||||
// Calculate the new strength value based on movement
|
||||
// Moving right increases, moving left decreases
|
||||
let newStrength = Number(initialStrength) + (deltaX * sensitivity);
|
||||
|
||||
// Limit the strength to reasonable bounds (now between -10 and 10)
|
||||
newStrength = Math.max(-10, Math.min(10, newStrength));
|
||||
newStrength = Number(newStrength.toFixed(2));
|
||||
|
||||
// Update the lora data
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||
|
||||
if (loraIndex >= 0) {
|
||||
// Update the appropriate strength property based on isClipStrength flag
|
||||
if (isClipStrength) {
|
||||
lorasData[loraIndex].clipStrength = newStrength;
|
||||
} else {
|
||||
lorasData[loraIndex].strength = newStrength;
|
||||
// Sync clipStrength if collapsed
|
||||
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
||||
}
|
||||
|
||||
// Update the widget value
|
||||
widget.value = formatLoraValue(lorasData);
|
||||
|
||||
// Force re-render to show updated strength value
|
||||
renderLoras(widget.value, widget);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to initialize drag operation
|
||||
const initDrag = (dragEl, name, widget, isClipStrength = false) => {
|
||||
let isDragging = false;
|
||||
let initialX = 0;
|
||||
let initialStrength = 0;
|
||||
|
||||
// Create a style element for drag cursor override if it doesn't exist
|
||||
if (!document.getElementById('comfy-lora-drag-style')) {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'comfy-lora-drag-style';
|
||||
styleEl.textContent = `
|
||||
body.comfy-lora-dragging,
|
||||
body.comfy-lora-dragging * {
|
||||
cursor: ew-resize !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Create a drag handler
|
||||
dragEl.addEventListener('mousedown', (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;
|
||||
}
|
||||
|
||||
// Store initial values
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraData = lorasData.find(l => l.name === name);
|
||||
|
||||
if (!loraData) return;
|
||||
|
||||
initialX = e.clientX;
|
||||
initialStrength = isClipStrength ? loraData.clipStrength : loraData.strength;
|
||||
isDragging = true;
|
||||
|
||||
// Add class to body to enforce cursor style globally
|
||||
document.body.classList.add('comfy-lora-dragging');
|
||||
|
||||
// Prevent text selection during drag
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Use the document for move and up events to ensure drag continues
|
||||
// even if mouse leaves the element
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Call the strength adjustment function
|
||||
handleStrengthDrag(name, initialStrength, initialX, e, widget, isClipStrength);
|
||||
|
||||
// Prevent showing the preview tooltip during drag
|
||||
previewTooltip.hide();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
// Remove the class to restore normal cursor behavior
|
||||
document.body.classList.remove('comfy-lora-dragging');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Function to create context menu
|
||||
const createContextMenu = (x, y, loraName, widget) => {
|
||||
// Hide preview tooltip first
|
||||
previewTooltip.hide();
|
||||
|
||||
// Remove existing context menu if any
|
||||
const existingMenu = document.querySelector('.comfy-lora-context-menu');
|
||||
if (existingMenu) {
|
||||
existingMenu.remove();
|
||||
}
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'comfy-lora-context-menu';
|
||||
Object.assign(menu.style, {
|
||||
position: 'fixed',
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
backgroundColor: 'rgba(30, 30, 30, 0.95)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '4px',
|
||||
padding: '4px 0',
|
||||
zIndex: 1000,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
|
||||
minWidth: '180px',
|
||||
});
|
||||
|
||||
// View on Civitai option with globe icon
|
||||
const viewOnCivitaiOption = createMenuItem(
|
||||
'View on Civitai',
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
|
||||
async () => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
|
||||
try {
|
||||
// Get Civitai URL from API
|
||||
const response = await api.fetchApi(`/lora-civitai-url?name=${encodeURIComponent(loraName)}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || 'Failed to get Civitai URL');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.civitai_url) {
|
||||
// Open the URL in a new tab
|
||||
window.open(data.civitai_url, '_blank');
|
||||
} else {
|
||||
// Show error message if no Civitai URL
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'warning',
|
||||
summary: 'Not Found',
|
||||
detail: 'This LoRA has no associated Civitai URL',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
alert('This LoRA has no associated Civitai URL');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Civitai URL:', error);
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message || 'Failed to get Civitai URL',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (error.message || 'Failed to get Civitai URL'));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Delete option with trash icon
|
||||
const deleteOption = createMenuItem(
|
||||
'Delete',
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>',
|
||||
() => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
|
||||
const lorasData = parseLoraValue(widget.value).filter(l => l.name !== loraName);
|
||||
widget.value = formatLoraValue(lorasData);
|
||||
|
||||
if (widget.callback) {
|
||||
widget.callback(widget.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Save recipe option with bookmark icon
|
||||
const saveOption = createMenuItem(
|
||||
'Save Recipe',
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>',
|
||||
() => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
saveRecipeDirectly(widget);
|
||||
}
|
||||
);
|
||||
|
||||
// Add separator
|
||||
const separator = document.createElement('div');
|
||||
Object.assign(separator.style, {
|
||||
margin: '4px 0',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
});
|
||||
|
||||
menu.appendChild(viewOnCivitaiOption);
|
||||
menu.appendChild(deleteOption);
|
||||
menu.appendChild(separator);
|
||||
menu.appendChild(saveOption);
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Close menu when clicking outside
|
||||
const closeMenu = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||||
};
|
||||
|
||||
// Function to render loras from data
|
||||
const renderLoras = (value, widget) => {
|
||||
// Clear existing content
|
||||
@@ -635,7 +70,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
container.appendChild(emptyMessage);
|
||||
|
||||
// Set fixed height for empty state
|
||||
updateWidgetHeight(EMPTY_CONTAINER_HEIGHT);
|
||||
updateWidgetHeight(container, EMPTY_CONTAINER_HEIGHT, defaultHeight, node);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -812,7 +247,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
});
|
||||
|
||||
// Initialize drag functionality for strength adjustment
|
||||
initDrag(loraEl, name, widget, false);
|
||||
initDrag(loraEl, name, widget, false, previewTooltip, renderLoras);
|
||||
|
||||
// Remove the preview tooltip events from loraEl
|
||||
loraEl.onmouseenter = () => {
|
||||
@@ -827,7 +262,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
loraEl.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
createContextMenu(e.clientX, e.clientY, name, widget);
|
||||
createContextMenu(e.clientX, e.clientY, name, widget, previewTooltip, renderLoras);
|
||||
});
|
||||
|
||||
// Create strength control
|
||||
@@ -1133,7 +568,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
};
|
||||
|
||||
// Add drag functionality to clip entry
|
||||
initDrag(clipEl, name, widget, true);
|
||||
initDrag(clipEl, name, widget, true, previewTooltip, renderLoras);
|
||||
|
||||
container.appendChild(clipEl);
|
||||
}
|
||||
@@ -1141,7 +576,7 @@ 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, 8) * LORA_ENTRY_HEIGHT);
|
||||
updateWidgetHeight(calculatedHeight);
|
||||
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
|
||||
};
|
||||
|
||||
// Store the value in a variable to avoid recursion
|
||||
@@ -1227,78 +662,4 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
};
|
||||
|
||||
return { minWidth: 400, minHeight: defaultHeight, widget };
|
||||
}
|
||||
|
||||
// Function to directly save the recipe without dialog
|
||||
async function saveRecipeDirectly(widget) {
|
||||
try {
|
||||
const prompt = await app.graphToPrompt();
|
||||
console.log(prompt);
|
||||
// Show loading toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Saving Recipe',
|
||||
detail: 'Please wait...',
|
||||
life: 2000
|
||||
});
|
||||
}
|
||||
|
||||
// Send the request to the backend API without workflow data
|
||||
const response = await fetch('/api/recipes/save-from-widget', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Show result toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
if (result.success) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Recipe Saved',
|
||||
detail: 'Recipe has been saved successfully',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: result.error || 'Failed to save recipe',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving recipe:', error);
|
||||
|
||||
// Show error toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to save recipe: ' + (error.message || 'Unknown error'),
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if clip entry should be shown - now based on expanded property or initial diff values
|
||||
const shouldShowClipEntry = (loraData) => {
|
||||
// If expanded property exists, use that
|
||||
if (loraData.hasOwnProperty('expanded')) {
|
||||
return loraData.expanded;
|
||||
}
|
||||
// Otherwise use the legacy logic - if values differ, it should be expanded
|
||||
return Number(loraData.strength) !== Number(loraData.clipStrength);
|
||||
}
|
||||
|
||||
// Helper function to sync clipStrength with strength when collapsed
|
||||
const syncClipStrengthIfCollapsed = (loraData) => {
|
||||
// If not expanded (collapsed), sync clipStrength with strength
|
||||
if (loraData.hasOwnProperty('expanded') && !loraData.expanded) {
|
||||
loraData.clipStrength = loraData.strength;
|
||||
}
|
||||
return loraData;
|
||||
};
|
||||
}
|
||||
303
web/comfyui/loras_widget_components.js
Normal file
303
web/comfyui/loras_widget_components.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
// Function to create toggle element
|
||||
export function createToggle(active, onChange) {
|
||||
const toggle = document.createElement("div");
|
||||
toggle.className = "comfy-lora-toggle";
|
||||
|
||||
updateToggleStyle(toggle, active);
|
||||
|
||||
toggle.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
onChange(!active);
|
||||
});
|
||||
|
||||
return toggle;
|
||||
}
|
||||
|
||||
// Helper function to update toggle style
|
||||
export function updateToggleStyle(toggleEl, active) {
|
||||
Object.assign(toggleEl.style, {
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
backgroundColor: active ? "rgba(66, 153, 225, 0.9)" : "rgba(45, 55, 72, 0.7)",
|
||||
border: `1px solid ${active ? "rgba(66, 153, 225, 0.9)" : "rgba(226, 232, 240, 0.2)"}`,
|
||||
});
|
||||
|
||||
// Add hover effect
|
||||
toggleEl.onmouseenter = () => {
|
||||
toggleEl.style.transform = "scale(1.05)";
|
||||
toggleEl.style.boxShadow = "0 2px 4px rgba(0,0,0,0.15)";
|
||||
};
|
||||
|
||||
toggleEl.onmouseleave = () => {
|
||||
toggleEl.style.transform = "scale(1)";
|
||||
toggleEl.style.boxShadow = "none";
|
||||
};
|
||||
}
|
||||
|
||||
// Create arrow button for strength adjustment
|
||||
export function createArrowButton(direction, onClick) {
|
||||
const button = document.createElement("div");
|
||||
button.className = `comfy-lora-arrow comfy-lora-arrow-${direction}`;
|
||||
|
||||
Object.assign(button.style, {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
fontSize: "12px",
|
||||
color: "rgba(226, 232, 240, 0.8)",
|
||||
transition: "all 0.2s ease",
|
||||
});
|
||||
|
||||
button.textContent = direction === "left" ? "◀" : "▶";
|
||||
|
||||
button.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
});
|
||||
|
||||
// Add hover effect
|
||||
button.onmouseenter = () => {
|
||||
button.style.color = "white";
|
||||
button.style.transform = "scale(1.2)";
|
||||
};
|
||||
|
||||
button.onmouseleave = () => {
|
||||
button.style.color = "rgba(226, 232, 240, 0.8)";
|
||||
button.style.transform = "scale(1)";
|
||||
};
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Function to create menu item
|
||||
export function createMenuItem(text, icon, onClick) {
|
||||
const menuItem = document.createElement('div');
|
||||
Object.assign(menuItem.style, {
|
||||
padding: '6px 20px',
|
||||
cursor: 'pointer',
|
||||
color: 'rgba(226, 232, 240, 0.9)',
|
||||
fontSize: '13px',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
// Create icon element
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.innerHTML = icon;
|
||||
Object.assign(iconEl.style, {
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
// Create text element
|
||||
const textEl = document.createElement('span');
|
||||
textEl.textContent = text;
|
||||
|
||||
menuItem.appendChild(iconEl);
|
||||
menuItem.appendChild(textEl);
|
||||
|
||||
menuItem.addEventListener('mouseenter', () => {
|
||||
menuItem.style.backgroundColor = 'rgba(66, 153, 225, 0.2)';
|
||||
});
|
||||
|
||||
menuItem.addEventListener('mouseleave', () => {
|
||||
menuItem.style.backgroundColor = 'transparent';
|
||||
});
|
||||
|
||||
if (onClick) {
|
||||
menuItem.addEventListener('click', onClick);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
// Preview tooltip class
|
||||
export class PreviewTooltip {
|
||||
constructor() {
|
||||
this.element = document.createElement('div');
|
||||
Object.assign(this.element.style, {
|
||||
position: 'fixed',
|
||||
zIndex: 9999,
|
||||
background: 'rgba(0, 0, 0, 0.85)',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
display: 'none',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '300px',
|
||||
});
|
||||
document.body.appendChild(this.element);
|
||||
this.hideTimeout = null;
|
||||
|
||||
// Add global click event to hide tooltip
|
||||
document.addEventListener('click', () => this.hide());
|
||||
|
||||
// Add scroll event listener
|
||||
document.addEventListener('scroll', () => this.hide(), true);
|
||||
}
|
||||
|
||||
async show(loraName, x, y) {
|
||||
try {
|
||||
// Clear previous hide timer
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = null;
|
||||
}
|
||||
|
||||
// Don't redisplay the same lora preview
|
||||
if (this.element.style.display === 'block' && this.currentLora === loraName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLora = loraName;
|
||||
|
||||
// Get preview URL
|
||||
const response = await api.fetchApi(`/lora-preview-url?name=${encodeURIComponent(loraName)}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch preview URL');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.preview_url) {
|
||||
throw new Error('No preview available');
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
while (this.element.firstChild) {
|
||||
this.element.removeChild(this.element.firstChild);
|
||||
}
|
||||
|
||||
// Create media container with relative positioning
|
||||
const mediaContainer = document.createElement('div');
|
||||
Object.assign(mediaContainer.style, {
|
||||
position: 'relative',
|
||||
maxWidth: '300px',
|
||||
maxHeight: '300px',
|
||||
});
|
||||
|
||||
const isVideo = data.preview_url.endsWith('.mp4');
|
||||
const mediaElement = isVideo ? document.createElement('video') : document.createElement('img');
|
||||
|
||||
Object.assign(mediaElement.style, {
|
||||
maxWidth: '300px',
|
||||
maxHeight: '300px',
|
||||
objectFit: 'contain',
|
||||
display: 'block',
|
||||
});
|
||||
|
||||
if (isVideo) {
|
||||
mediaElement.autoplay = true;
|
||||
mediaElement.loop = true;
|
||||
mediaElement.muted = true;
|
||||
mediaElement.controls = false;
|
||||
}
|
||||
|
||||
mediaElement.src = data.preview_url;
|
||||
|
||||
// Create name label with absolute positioning
|
||||
const nameLabel = document.createElement('div');
|
||||
nameLabel.textContent = loraName;
|
||||
Object.assign(nameLabel.style, {
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
left: '0',
|
||||
right: '0',
|
||||
padding: '8px',
|
||||
color: 'rgba(255, 255, 255, 0.95)',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif",
|
||||
background: 'linear-gradient(transparent, rgba(0, 0, 0, 0.8))',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textAlign: 'center',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
});
|
||||
|
||||
mediaContainer.appendChild(mediaElement);
|
||||
mediaContainer.appendChild(nameLabel);
|
||||
this.element.appendChild(mediaContainer);
|
||||
|
||||
// Add fade-in effect
|
||||
this.element.style.opacity = '0';
|
||||
this.element.style.display = 'block';
|
||||
this.position(x, y);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.element.style.transition = 'opacity 0.15s ease';
|
||||
this.element.style.opacity = '1';
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to load preview:', error);
|
||||
}
|
||||
}
|
||||
|
||||
position(x, y) {
|
||||
// Ensure preview box doesn't exceed viewport boundaries
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let left = x + 10; // Default 10px offset to the right of mouse
|
||||
let top = y + 10; // Default 10px offset below mouse
|
||||
|
||||
// Check right boundary
|
||||
if (left + rect.width > viewportWidth) {
|
||||
left = x - rect.width - 10;
|
||||
}
|
||||
|
||||
// Check bottom boundary
|
||||
if (top + rect.height > viewportHeight) {
|
||||
top = y - rect.height - 10;
|
||||
}
|
||||
|
||||
Object.assign(this.element.style, {
|
||||
left: `${left}px`,
|
||||
top: `${top}px`
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
// Use fade-out effect
|
||||
if (this.element.style.display === 'block') {
|
||||
this.element.style.opacity = '0';
|
||||
this.hideTimeout = setTimeout(() => {
|
||||
this.element.style.display = 'none';
|
||||
this.currentLora = null;
|
||||
// Stop video playback
|
||||
const video = this.element.querySelector('video');
|
||||
if (video) {
|
||||
video.pause();
|
||||
}
|
||||
this.hideTimeout = null;
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
}
|
||||
// Remove all event listeners
|
||||
document.removeEventListener('click', () => this.hide());
|
||||
document.removeEventListener('scroll', () => this.hide(), true);
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
257
web/comfyui/loras_widget_events.js
Normal file
257
web/comfyui/loras_widget_events.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { createMenuItem } from "./loras_widget_components.js";
|
||||
import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly } from "./loras_widget_utils.js";
|
||||
|
||||
// Function to handle strength adjustment via dragging
|
||||
export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) {
|
||||
// Calculate drag sensitivity (how much the strength changes per pixel)
|
||||
// Using 0.01 per 10 pixels of movement
|
||||
const sensitivity = 0.001;
|
||||
|
||||
// Get the current mouse position
|
||||
const currentX = event.clientX;
|
||||
|
||||
// Calculate the distance moved
|
||||
const deltaX = currentX - initialX;
|
||||
|
||||
// Calculate the new strength value based on movement
|
||||
// Moving right increases, moving left decreases
|
||||
let newStrength = Number(initialStrength) + (deltaX * sensitivity);
|
||||
|
||||
// Limit the strength to reasonable bounds (now between -10 and 10)
|
||||
newStrength = Math.max(-10, Math.min(10, newStrength));
|
||||
newStrength = Number(newStrength.toFixed(2));
|
||||
|
||||
// Update the lora data
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||
|
||||
if (loraIndex >= 0) {
|
||||
// Update the appropriate strength property based on isClipStrength flag
|
||||
if (isClipStrength) {
|
||||
lorasData[loraIndex].clipStrength = newStrength;
|
||||
} else {
|
||||
lorasData[loraIndex].strength = newStrength;
|
||||
// Sync clipStrength if collapsed
|
||||
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
||||
}
|
||||
|
||||
// Update the widget value
|
||||
widget.value = formatLoraValue(lorasData);
|
||||
|
||||
// Force re-render via callback
|
||||
if (widget.callback) {
|
||||
widget.callback(widget.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize drag operation
|
||||
export function initDrag(dragEl, name, widget, isClipStrength = false, previewTooltip, renderFunction) {
|
||||
let isDragging = false;
|
||||
let initialX = 0;
|
||||
let initialStrength = 0;
|
||||
|
||||
// Create a style element for drag cursor override if it doesn't exist
|
||||
if (!document.getElementById('comfy-lora-drag-style')) {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'comfy-lora-drag-style';
|
||||
styleEl.textContent = `
|
||||
body.comfy-lora-dragging,
|
||||
body.comfy-lora-dragging * {
|
||||
cursor: ew-resize !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Create a drag handler
|
||||
dragEl.addEventListener('mousedown', (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;
|
||||
}
|
||||
|
||||
// Store initial values
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraData = lorasData.find(l => l.name === name);
|
||||
|
||||
if (!loraData) return;
|
||||
|
||||
initialX = e.clientX;
|
||||
initialStrength = isClipStrength ? loraData.clipStrength : loraData.strength;
|
||||
isDragging = true;
|
||||
|
||||
// Add class to body to enforce cursor style globally
|
||||
document.body.classList.add('comfy-lora-dragging');
|
||||
|
||||
// Prevent text selection during drag
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Use the document for move and up events to ensure drag continues
|
||||
// even if mouse leaves the element
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Call the strength adjustment function
|
||||
handleStrengthDrag(name, initialStrength, initialX, e, widget, isClipStrength);
|
||||
|
||||
// Force re-render to show updated strength value
|
||||
if (renderFunction) {
|
||||
renderFunction(widget.value, widget);
|
||||
}
|
||||
|
||||
// Prevent showing the preview tooltip during drag
|
||||
if (previewTooltip) {
|
||||
previewTooltip.hide();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
// Remove the class to restore normal cursor behavior
|
||||
document.body.classList.remove('comfy-lora-dragging');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to create context menu
|
||||
export function createContextMenu(x, y, loraName, widget, previewTooltip, renderFunction) {
|
||||
// Hide preview tooltip first
|
||||
if (previewTooltip) {
|
||||
previewTooltip.hide();
|
||||
}
|
||||
|
||||
// Remove existing context menu if any
|
||||
const existingMenu = document.querySelector('.comfy-lora-context-menu');
|
||||
if (existingMenu) {
|
||||
existingMenu.remove();
|
||||
}
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'comfy-lora-context-menu';
|
||||
Object.assign(menu.style, {
|
||||
position: 'fixed',
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
backgroundColor: 'rgba(30, 30, 30, 0.95)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '4px',
|
||||
padding: '4px 0',
|
||||
zIndex: 1000,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
|
||||
minWidth: '180px',
|
||||
});
|
||||
|
||||
// View on Civitai option with globe icon
|
||||
const viewOnCivitaiOption = createMenuItem(
|
||||
'View on Civitai',
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
|
||||
async () => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
|
||||
try {
|
||||
// Get Civitai URL from API
|
||||
const response = await api.fetchApi(`/lora-civitai-url?name=${encodeURIComponent(loraName)}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || 'Failed to get Civitai URL');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.civitai_url) {
|
||||
// Open the URL in a new tab
|
||||
window.open(data.civitai_url, '_blank');
|
||||
} else {
|
||||
// Show error message if no Civitai URL
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'warning',
|
||||
summary: 'Not Found',
|
||||
detail: 'This LoRA has no associated Civitai URL',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
alert('This LoRA has no associated Civitai URL');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Civitai URL:', error);
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message || 'Failed to get Civitai URL',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (error.message || 'Failed to get Civitai URL'));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Delete option with trash icon
|
||||
const deleteOption = createMenuItem(
|
||||
'Delete',
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>',
|
||||
() => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
|
||||
const lorasData = parseLoraValue(widget.value).filter(l => l.name !== loraName);
|
||||
widget.value = formatLoraValue(lorasData);
|
||||
|
||||
if (widget.callback) {
|
||||
widget.callback(widget.value);
|
||||
}
|
||||
|
||||
// Re-render
|
||||
if (renderFunction) {
|
||||
renderFunction(widget.value, widget);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Save recipe option with bookmark icon
|
||||
const saveOption = createMenuItem(
|
||||
'Save Recipe',
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>',
|
||||
() => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
saveRecipeDirectly();
|
||||
}
|
||||
);
|
||||
|
||||
// Add separator
|
||||
const separator = document.createElement('div');
|
||||
Object.assign(separator.style, {
|
||||
margin: '4px 0',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
});
|
||||
|
||||
menu.appendChild(viewOnCivitaiOption);
|
||||
menu.appendChild(deleteOption);
|
||||
menu.appendChild(separator);
|
||||
menu.appendChild(saveOption);
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Close menu when clicking outside
|
||||
const closeMenu = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||||
}
|
||||
109
web/comfyui/loras_widget_utils.js
Normal file
109
web/comfyui/loras_widget_utils.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
// Fixed sizes for component calculations
|
||||
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 HEADER_HEIGHT = 40; // Height of the header section
|
||||
export const CONTAINER_PADDING = 12; // Top and bottom padding
|
||||
export const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
|
||||
|
||||
// Parse LoRA entries from value
|
||||
export function parseLoraValue(value) {
|
||||
if (!value) return [];
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
// Format LoRA data
|
||||
export function formatLoraValue(loras) {
|
||||
return loras;
|
||||
}
|
||||
|
||||
// Function to update widget height consistently
|
||||
export function updateWidgetHeight(container, height, defaultHeight, node) {
|
||||
// Ensure minimum height
|
||||
const finalHeight = Math.max(defaultHeight, height);
|
||||
|
||||
// Update CSS variables
|
||||
container.style.setProperty('--comfy-widget-min-height', `${finalHeight}px`);
|
||||
container.style.setProperty('--comfy-widget-height', `${finalHeight}px`);
|
||||
|
||||
// Force node to update size after a short delay to ensure DOM is updated
|
||||
if (node) {
|
||||
setTimeout(() => {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if clip entry should be shown - now based on expanded property or initial diff values
|
||||
export function shouldShowClipEntry(loraData) {
|
||||
// If expanded property exists, use that
|
||||
if (loraData.hasOwnProperty('expanded')) {
|
||||
return loraData.expanded;
|
||||
}
|
||||
// Otherwise use the legacy logic - if values differ, it should be expanded
|
||||
return Number(loraData.strength) !== Number(loraData.clipStrength);
|
||||
}
|
||||
|
||||
// Helper function to sync clipStrength with strength when collapsed
|
||||
export function syncClipStrengthIfCollapsed(loraData) {
|
||||
// If not expanded (collapsed), sync clipStrength with strength
|
||||
if (loraData.hasOwnProperty('expanded') && !loraData.expanded) {
|
||||
loraData.clipStrength = loraData.strength;
|
||||
}
|
||||
return loraData;
|
||||
}
|
||||
|
||||
// Function to directly save the recipe without dialog
|
||||
export async function saveRecipeDirectly() {
|
||||
try {
|
||||
const prompt = await app.graphToPrompt();
|
||||
// Show loading toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Saving Recipe',
|
||||
detail: 'Please wait...',
|
||||
life: 2000
|
||||
});
|
||||
}
|
||||
|
||||
// Send the request to the backend API
|
||||
const response = await fetch('/api/recipes/save-from-widget', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Show result toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
if (result.success) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Recipe Saved',
|
||||
detail: 'Recipe has been saved successfully',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: result.error || 'Failed to save recipe',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving recipe:', error);
|
||||
|
||||
// Show error toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to save recipe: ' + (error.message || 'Unknown error'),
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user