mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Refactor Lora widget and dynamic module loading
- Updated lora_loader.js to dynamically import the appropriate loras widget based on ComfyUI version, enhancing compatibility and maintainability. - Enhanced loras_widget.js with improved height management and styling for better user experience. - Introduced utility functions in utils.js for version checking and dynamic imports, streamlining widget loading processes. - Improved overall structure and readability of the code, ensuring better performance and easier future updates.
This commit is contained in:
@@ -5,6 +5,13 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
// Create container for loras
|
||||
const container = document.createElement("div");
|
||||
container.className = "comfy-loras-container";
|
||||
|
||||
// Set initial height using CSS variables approach
|
||||
const defaultHeight = 200;
|
||||
container.style.setProperty('--comfy-widget-min-height', `${defaultHeight}px`);
|
||||
container.style.setProperty('--comfy-widget-max-height', `${defaultHeight * 2}px`);
|
||||
container.style.setProperty('--comfy-widget-height', `${defaultHeight}px`);
|
||||
|
||||
Object.assign(container.style, {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@@ -13,11 +20,19 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
backgroundColor: "rgba(40, 44, 52, 0.6)",
|
||||
borderRadius: "6px",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
overflow: "auto"
|
||||
});
|
||||
|
||||
// Initialize default value
|
||||
const defaultValue = opts?.defaultVal || [];
|
||||
|
||||
// Fixed sizes for component calculations
|
||||
const LORA_ENTRY_HEIGHT = 44; // Height of a single lora 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
|
||||
|
||||
// Parse LoRA entries from value
|
||||
const parseLoraValue = (value) => {
|
||||
if (!value) return [];
|
||||
@@ -29,6 +44,23 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
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");
|
||||
@@ -107,7 +139,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
return button;
|
||||
};
|
||||
|
||||
// 添加预览弹窗组件
|
||||
// Preview tooltip class
|
||||
class PreviewTooltip {
|
||||
constructor() {
|
||||
this.element = document.createElement('div');
|
||||
@@ -122,31 +154,31 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
maxWidth: '300px',
|
||||
});
|
||||
document.body.appendChild(this.element);
|
||||
this.hideTimeout = null; // 添加超时处理变量
|
||||
this.hideTimeout = null;
|
||||
|
||||
// 添加全局点击事件来隐藏tooltip
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 如果已经显示同一个lora的预览,则不重复显示
|
||||
// Don't redisplay the same lora preview
|
||||
if (this.element.style.display === 'block' && this.currentLora === loraName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLora = loraName;
|
||||
|
||||
// 获取预览URL
|
||||
// Get preview URL
|
||||
const response = await api.fetchApi(`/lora-preview-url?name=${encodeURIComponent(loraName)}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
@@ -160,7 +192,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
throw new Error('No preview available');
|
||||
}
|
||||
|
||||
// 清除现有内容
|
||||
// Clear existing content
|
||||
while (this.element.firstChild) {
|
||||
this.element.removeChild(this.element.firstChild);
|
||||
}
|
||||
@@ -217,7 +249,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
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);
|
||||
@@ -232,20 +264,20 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
}
|
||||
|
||||
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; // 默认在鼠标右侧偏移10px
|
||||
let top = y + 10; // 默认在鼠标下方偏移10px
|
||||
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;
|
||||
}
|
||||
@@ -257,13 +289,13 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -277,14 +309,14 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
}
|
||||
// 移除所有事件监听器
|
||||
// Remove all event listeners
|
||||
document.removeEventListener('click', () => this.hide());
|
||||
document.removeEventListener('scroll', () => this.hide(), true);
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建预览tooltip实例
|
||||
// Create preview tooltip instance
|
||||
const previewTooltip = new PreviewTooltip();
|
||||
|
||||
// Function to create menu item
|
||||
@@ -357,7 +389,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
padding: '4px 0',
|
||||
zIndex: 1000,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
|
||||
minWidth: '180px',
|
||||
minWidth: '180px',
|
||||
});
|
||||
|
||||
// View on Civitai option with globe icon
|
||||
@@ -447,7 +479,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
});
|
||||
|
||||
menu.appendChild(viewOnCivitaiOption); // Add the new menu option
|
||||
menu.appendChild(viewOnCivitaiOption);
|
||||
menu.appendChild(deleteOption);
|
||||
menu.appendChild(separator);
|
||||
menu.appendChild(saveOption);
|
||||
@@ -483,12 +515,16 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
padding: "20px 0",
|
||||
color: "rgba(226, 232, 240, 0.8)",
|
||||
fontStyle: "italic",
|
||||
userSelect: "none", // Add this line to prevent text selection
|
||||
WebkitUserSelect: "none", // For Safari support
|
||||
MozUserSelect: "none", // For Firefox support
|
||||
msUserSelect: "none", // For IE/Edge support
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
width: "100%"
|
||||
});
|
||||
container.appendChild(emptyMessage);
|
||||
|
||||
// Set fixed height for empty state
|
||||
updateWidgetHeight(EMPTY_CONTAINER_HEIGHT);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -522,10 +558,10 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
color: "rgba(226, 232, 240, 0.8)",
|
||||
fontSize: "13px",
|
||||
marginLeft: "8px",
|
||||
userSelect: "none", // Add this line to prevent text selection
|
||||
WebkitUserSelect: "none", // For Safari support
|
||||
MozUserSelect: "none", // For Firefox support
|
||||
msUserSelect: "none", // For IE/Edge support
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
});
|
||||
|
||||
const toggleContainer = document.createElement("div");
|
||||
@@ -543,10 +579,10 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
color: "rgba(226, 232, 240, 0.8)",
|
||||
fontSize: "13px",
|
||||
marginRight: "8px",
|
||||
userSelect: "none", // Add this line to prevent text selection
|
||||
WebkitUserSelect: "none", // For Safari support
|
||||
MozUserSelect: "none", // For Firefox support
|
||||
msUserSelect: "none", // For IE/Edge support
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
});
|
||||
|
||||
header.appendChild(toggleContainer);
|
||||
@@ -595,11 +631,11 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
whiteSpace: "nowrap",
|
||||
color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)",
|
||||
fontSize: "13px",
|
||||
cursor: "pointer", // Add pointer cursor to indicate hoverable area
|
||||
userSelect: "none", // Add this line to prevent text selection
|
||||
WebkitUserSelect: "none", // For Safari support
|
||||
MozUserSelect: "none", // For Firefox support
|
||||
msUserSelect: "none", // For IE/Edge support
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
});
|
||||
|
||||
// Move preview tooltip events to nameEl instead of loraEl
|
||||
@@ -645,7 +681,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||
|
||||
if (loraIndex >= 0) {
|
||||
lorasData[loraIndex].strength = (lorasData[loraIndex].strength - 0.05).toFixed(2);
|
||||
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) - 0.05).toFixed(2);
|
||||
|
||||
const newValue = formatLoraValue(lorasData);
|
||||
widget.value = newValue;
|
||||
@@ -669,7 +705,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
outline: "none",
|
||||
});
|
||||
|
||||
// 添加hover效果
|
||||
// Add hover effect
|
||||
strengthEl.addEventListener('mouseenter', () => {
|
||||
strengthEl.style.border = "1px solid rgba(226, 232, 240, 0.2)";
|
||||
});
|
||||
@@ -680,11 +716,11 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
}
|
||||
});
|
||||
|
||||
// 处理焦点
|
||||
// Handle focus
|
||||
strengthEl.addEventListener('focus', () => {
|
||||
strengthEl.style.border = "1px solid rgba(66, 153, 225, 0.6)";
|
||||
strengthEl.style.background = "rgba(0, 0, 0, 0.2)";
|
||||
// 自动选中所有内容
|
||||
// Auto-select all content
|
||||
strengthEl.select();
|
||||
});
|
||||
|
||||
@@ -693,29 +729,29 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
strengthEl.style.background = "none";
|
||||
});
|
||||
|
||||
// 处理输入变化
|
||||
// Handle input changes
|
||||
strengthEl.addEventListener('change', () => {
|
||||
let newValue = parseFloat(strengthEl.value);
|
||||
|
||||
// 验证输入
|
||||
// Validate input
|
||||
if (isNaN(newValue)) {
|
||||
newValue = 1.0;
|
||||
}
|
||||
|
||||
// 更新数值
|
||||
// Update value
|
||||
const lorasData = parseLoraValue(widget.value);
|
||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||
|
||||
if (loraIndex >= 0) {
|
||||
lorasData[loraIndex].strength = newValue.toFixed(2);
|
||||
|
||||
// 更新值并触发回调
|
||||
// Update value and trigger callback
|
||||
const newLorasValue = formatLoraValue(lorasData);
|
||||
widget.value = newLorasValue;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理按键事件
|
||||
// Handle key events
|
||||
strengthEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
strengthEl.blur();
|
||||
@@ -757,13 +793,17 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
|
||||
container.appendChild(loraEl);
|
||||
});
|
||||
|
||||
// Calculate height based on number of loras and fixed sizes
|
||||
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (lorasData.length * LORA_ENTRY_HEIGHT);
|
||||
updateWidgetHeight(calculatedHeight);
|
||||
};
|
||||
|
||||
// Store the value in a variable to avoid recursion
|
||||
let widgetValue = defaultValue;
|
||||
|
||||
// Create widget with initial properties
|
||||
const widget = node.addDOMWidget(name, "loras", container, {
|
||||
// Create widget with new DOM Widget API
|
||||
const widget = node.addDOMWidget(name, "custom", container, {
|
||||
getValue: function() {
|
||||
return widgetValue;
|
||||
},
|
||||
@@ -778,29 +818,28 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
|
||||
widgetValue = uniqueValue;
|
||||
renderLoras(widgetValue, widget);
|
||||
|
||||
// Update container height after rendering
|
||||
requestAnimationFrame(() => {
|
||||
const minHeight = this.getMinHeight();
|
||||
container.style.height = `${minHeight}px`;
|
||||
|
||||
// Force node to update size
|
||||
node.setSize([node.size[0], node.computeSize()[1]]);
|
||||
node.setDirtyCanvas(true, true);
|
||||
});
|
||||
},
|
||||
getMinHeight: function() {
|
||||
// Calculate height based on content
|
||||
const lorasCount = parseLoraValue(widgetValue).length;
|
||||
return Math.max(
|
||||
100,
|
||||
lorasCount > 0 ? 60 + lorasCount * 44 : 60
|
||||
);
|
||||
return parseInt(container.style.getPropertyValue('--comfy-widget-min-height')) || defaultHeight;
|
||||
},
|
||||
getMaxHeight: function() {
|
||||
return parseInt(container.style.getPropertyValue('--comfy-widget-max-height')) || defaultHeight * 2;
|
||||
},
|
||||
getHeight: function() {
|
||||
return parseInt(container.style.getPropertyValue('--comfy-widget-height')) || defaultHeight;
|
||||
},
|
||||
hideOnZoom: true,
|
||||
selectOn: ['click', 'focus'],
|
||||
afterResize: function(node) {
|
||||
// Re-render after node resize
|
||||
if (this.value && this.value.length > 0) {
|
||||
renderLoras(this.value, this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
widget.value = defaultValue;
|
||||
|
||||
|
||||
widget.callback = callback;
|
||||
|
||||
widget.serializeValue = () => {
|
||||
@@ -816,7 +855,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
||||
previewTooltip.cleanup();
|
||||
};
|
||||
|
||||
return { minWidth: 400, minHeight: 200, widget };
|
||||
return { minWidth: 400, minHeight: defaultHeight, widget };
|
||||
}
|
||||
|
||||
// Function to directly save the recipe without dialog
|
||||
@@ -824,7 +863,6 @@ async function saveRecipeDirectly(widget) {
|
||||
try {
|
||||
// Get the workflow data from the ComfyUI app
|
||||
const prompt = await app.graphToPrompt();
|
||||
console.log('Prompt:', prompt);
|
||||
|
||||
// Show loading toast
|
||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||
@@ -879,4 +917,4 @@ async function saveRecipeDirectly(widget) {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user