mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-26 15:38:52 -03:00
Compare commits
2 Commits
ee84b30023
...
ee765a6d22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee765a6d22 | ||
|
|
c02f603ed2 |
@@ -7,6 +7,7 @@ import { translate } from '../utils/i18nHelpers.js';
|
|||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
import { bulkManager } from '../managers/BulkManager.js';
|
import { bulkManager } from '../managers/BulkManager.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
import { escapeHtml, escapeAttribute } from './shared/utils.js';
|
||||||
|
|
||||||
export class SidebarManager {
|
export class SidebarManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -1294,15 +1295,19 @@ export class SidebarManager {
|
|||||||
const isExpanded = this.expandedNodes.has(currentPath);
|
const isExpanded = this.expandedNodes.has(currentPath);
|
||||||
const isSelected = this.selectedPath === currentPath;
|
const isSelected = this.selectedPath === currentPath;
|
||||||
|
|
||||||
|
const escapedPath = escapeAttribute(currentPath);
|
||||||
|
const escapedFolderName = escapeHtml(folderName);
|
||||||
|
const escapedTitle = escapeAttribute(folderName);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="sidebar-tree-node" data-path="${currentPath}">
|
<div class="sidebar-tree-node" data-path="${escapedPath}">
|
||||||
<div class="sidebar-tree-node-content ${isSelected ? 'selected' : ''}" data-path="${currentPath}">
|
<div class="sidebar-tree-node-content ${isSelected ? 'selected' : ''}" data-path="${escapedPath}">
|
||||||
<div class="sidebar-tree-expand-icon ${isExpanded ? 'expanded' : ''}"
|
<div class="sidebar-tree-expand-icon ${isExpanded ? 'expanded' : ''}"
|
||||||
style="${hasChildren ? '' : 'opacity: 0; pointer-events: none;'}">
|
style="${hasChildren ? '' : 'opacity: 0; pointer-events: none;'}">
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</div>
|
</div>
|
||||||
<i class="fas fa-folder sidebar-tree-folder-icon"></i>
|
<i class="fas fa-folder sidebar-tree-folder-icon"></i>
|
||||||
<div class="sidebar-tree-folder-name" title="${folderName}">${folderName}</div>
|
<div class="sidebar-tree-folder-name" title="${escapedTitle}">${escapedFolderName}</div>
|
||||||
</div>
|
</div>
|
||||||
${hasChildren ? `
|
${hasChildren ? `
|
||||||
<div class="sidebar-tree-children ${isExpanded ? 'expanded' : ''}">
|
<div class="sidebar-tree-children ${isExpanded ? 'expanded' : ''}">
|
||||||
@@ -1342,12 +1347,15 @@ export class SidebarManager {
|
|||||||
const foldersHtml = this.foldersList.map(folder => {
|
const foldersHtml = this.foldersList.map(folder => {
|
||||||
const displayName = folder === '' ? '/' : folder;
|
const displayName = folder === '' ? '/' : folder;
|
||||||
const isSelected = this.selectedPath === folder;
|
const isSelected = this.selectedPath === folder;
|
||||||
|
const escapedPath = escapeAttribute(folder);
|
||||||
|
const escapedDisplayName = escapeHtml(displayName);
|
||||||
|
const escapedTitle = escapeAttribute(displayName);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="sidebar-folder-item ${isSelected ? 'selected' : ''}" data-path="${folder}">
|
<div class="sidebar-folder-item ${isSelected ? 'selected' : ''}" data-path="${escapedPath}">
|
||||||
<div class="sidebar-node-content" data-path="${folder}">
|
<div class="sidebar-node-content" data-path="${escapedPath}">
|
||||||
<i class="fas fa-folder sidebar-folder-icon"></i>
|
<i class="fas fa-folder sidebar-folder-icon"></i>
|
||||||
<div class="sidebar-folder-name" title="${displayName}">${displayName}</div>
|
<div class="sidebar-folder-name" title="${escapedTitle}">${escapedDisplayName}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1570,7 +1578,8 @@ export class SidebarManager {
|
|||||||
|
|
||||||
// Add selection to current path
|
// Add selection to current path
|
||||||
if (this.selectedPath !== null && this.selectedPath !== undefined) {
|
if (this.selectedPath !== null && this.selectedPath !== undefined) {
|
||||||
const selectedItem = folderTree.querySelector(`[data-path="${this.selectedPath}"]`);
|
const escapedPathSelector = CSS.escape(this.selectedPath);
|
||||||
|
const selectedItem = folderTree.querySelector(`[data-path="${escapedPathSelector}"]`);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
selectedItem.classList.add('selected');
|
selectedItem.classList.add('selected');
|
||||||
}
|
}
|
||||||
@@ -1581,7 +1590,8 @@ export class SidebarManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.selectedPath !== null && this.selectedPath !== undefined) {
|
if (this.selectedPath !== null && this.selectedPath !== undefined) {
|
||||||
const selectedNode = folderTree.querySelector(`[data-path="${this.selectedPath}"] .sidebar-tree-node-content`);
|
const escapedPathSelector = CSS.escape(this.selectedPath);
|
||||||
|
const selectedNode = folderTree.querySelector(`[data-path="${escapedPathSelector}"] .sidebar-tree-node-content`);
|
||||||
if (selectedNode) {
|
if (selectedNode) {
|
||||||
selectedNode.classList.add('selected');
|
selectedNode.classList.add('selected');
|
||||||
this.expandPathParents(this.selectedPath);
|
this.expandPathParents(this.selectedPath);
|
||||||
@@ -1655,7 +1665,7 @@ export class SidebarManager {
|
|||||||
const breadcrumbs = [`
|
const breadcrumbs = [`
|
||||||
<div class="breadcrumb-dropdown">
|
<div class="breadcrumb-dropdown">
|
||||||
<span class="sidebar-breadcrumb-item ${isRootSelected ? 'active' : ''}" data-path="">
|
<span class="sidebar-breadcrumb-item ${isRootSelected ? 'active' : ''}" data-path="">
|
||||||
<i class="fas fa-home"></i> ${this.apiClient.apiConfig.config.displayName} root
|
<i class="fas fa-home"></i> ${escapeHtml(this.apiClient.apiConfig.config.displayName)} root
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`];
|
`];
|
||||||
@@ -1675,8 +1685,8 @@ export class SidebarManager {
|
|||||||
</span>
|
</span>
|
||||||
<div class="breadcrumb-dropdown-menu">
|
<div class="breadcrumb-dropdown-menu">
|
||||||
${nextLevelFolders.map(folder => `
|
${nextLevelFolders.map(folder => `
|
||||||
<div class="breadcrumb-dropdown-item" data-path="${folder}">
|
<div class="breadcrumb-dropdown-item" data-path="${escapeAttribute(folder)}">
|
||||||
${folder}
|
${escapeHtml(folder)}
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -1692,12 +1702,14 @@ export class SidebarManager {
|
|||||||
|
|
||||||
// Get siblings for this level
|
// Get siblings for this level
|
||||||
const siblings = this.getSiblingFolders(parts, index);
|
const siblings = this.getSiblingFolders(parts, index);
|
||||||
|
const escapedCurrentPath = escapeAttribute(currentPath);
|
||||||
|
const escapedPart = escapeHtml(part);
|
||||||
|
|
||||||
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
||||||
breadcrumbs.push(`
|
breadcrumbs.push(`
|
||||||
<div class="breadcrumb-dropdown">
|
<div class="breadcrumb-dropdown">
|
||||||
<span class="sidebar-breadcrumb-item ${isLast ? 'active' : ''}" data-path="${currentPath}">
|
<span class="sidebar-breadcrumb-item ${isLast ? 'active' : ''}" data-path="${escapedCurrentPath}">
|
||||||
${part}
|
${escapedPart}
|
||||||
${siblings.length > 1 ? `
|
${siblings.length > 1 ? `
|
||||||
<span class="breadcrumb-dropdown-indicator">
|
<span class="breadcrumb-dropdown-indicator">
|
||||||
<i class="fas fa-caret-down"></i>
|
<i class="fas fa-caret-down"></i>
|
||||||
@@ -1706,11 +1718,14 @@ export class SidebarManager {
|
|||||||
</span>
|
</span>
|
||||||
${siblings.length > 1 ? `
|
${siblings.length > 1 ? `
|
||||||
<div class="breadcrumb-dropdown-menu">
|
<div class="breadcrumb-dropdown-menu">
|
||||||
${siblings.map(folder => `
|
${siblings.map(folder => {
|
||||||
|
const siblingPath = parts.slice(0, index).concat(folder).join('/');
|
||||||
|
return `
|
||||||
<div class="breadcrumb-dropdown-item ${folder === part ? 'active' : ''}"
|
<div class="breadcrumb-dropdown-item ${folder === part ? 'active' : ''}"
|
||||||
data-path="${currentPath.replace(part, folder)}">
|
data-path="${escapeAttribute(siblingPath)}">
|
||||||
${folder}
|
${escapeHtml(folder)}
|
||||||
</div>`).join('')
|
</div>`;
|
||||||
|
}).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -1732,8 +1747,8 @@ export class SidebarManager {
|
|||||||
</span>
|
</span>
|
||||||
<div class="breadcrumb-dropdown-menu">
|
<div class="breadcrumb-dropdown-menu">
|
||||||
${childFolders.map(folder => `
|
${childFolders.map(folder => `
|
||||||
<div class="breadcrumb-dropdown-item" data-path="${currentPath}/${folder}">
|
<div class="breadcrumb-dropdown-item" data-path="${escapeAttribute(currentPath + '/' + folder)}">
|
||||||
${folder}
|
${escapeHtml(folder)}
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
:spellcheck="spellcheck ?? false"
|
:spellcheck="spellcheck ?? false"
|
||||||
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]"
|
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
@wheel="onWheel"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="showClearButton"
|
v-if="showClearButton"
|
||||||
@@ -82,6 +83,59 @@ const onInput = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle mouse wheel events on the textarea.
|
||||||
|
* Forwards the event to the ComfyUI canvas for zooming when the textarea has no scrollbar,
|
||||||
|
* or handles pinch-to-zoom gestures.
|
||||||
|
*
|
||||||
|
* Logic aligns with ComfyUI's built-in multiline widget:
|
||||||
|
* src/renderer/extensions/vueNodes/widgets/composables/useStringWidget.ts
|
||||||
|
*/
|
||||||
|
const onWheel = (event: WheelEvent) => {
|
||||||
|
const textarea = textareaRef.value
|
||||||
|
if (!textarea) return
|
||||||
|
|
||||||
|
// Track if we have a vertical scrollbar
|
||||||
|
const canScrollY = textarea.scrollHeight > textarea.clientHeight
|
||||||
|
const deltaX = event.deltaX
|
||||||
|
const deltaY = event.deltaY
|
||||||
|
const isHorizontal = Math.abs(deltaX) > Math.abs(deltaY)
|
||||||
|
|
||||||
|
// Access ComfyUI app from global window
|
||||||
|
const app = (window as any).app
|
||||||
|
if (!app || !app.canvas || typeof app.canvas.processMouseWheel !== 'function') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Handle pinch-to-zoom (ctrlKey is true for pinch-to-zoom on most browsers)
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
app.canvas.processMouseWheel(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Horizontal scroll: pass to canvas (textareas usually don't scroll horizontally)
|
||||||
|
if (isHorizontal) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
app.canvas.processMouseWheel(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Vertical scrolling:
|
||||||
|
if (canScrollY) {
|
||||||
|
// If the textarea is scrollable, let it handle the wheel event but stop propagation
|
||||||
|
// to prevent the canvas from zooming while the user is trying to scroll the text
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
// If the textarea is NOT scrollable, forward the wheel event to the canvas
|
||||||
|
// so it can trigger zoom in/out
|
||||||
|
event.preventDefault()
|
||||||
|
app.canvas.processMouseWheel(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle external value changes (e.g., from "send lora to workflow")
|
// Handle external value changes (e.g., from "send lora to workflow")
|
||||||
const onExternalValueChange = (event: CustomEvent<{ value: string }>) => {
|
const onExternalValueChange = (event: CustomEvent<{ value: string }>) => {
|
||||||
updateHasTextState()
|
updateHasTextState()
|
||||||
|
|||||||
@@ -1988,14 +1988,14 @@ to { transform: rotate(360deg);
|
|||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-text-widget[data-v-7fb96eed] {
|
.autocomplete-text-widget[data-v-b3b00fdd] {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.input-wrapper[data-v-7fb96eed] {
|
.input-wrapper[data-v-b3b00fdd] {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2003,7 +2003,7 @@ to { transform: rotate(360deg);
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */
|
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */
|
||||||
.text-input[data-v-7fb96eed] {
|
.text-input[data-v-b3b00fdd] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--comfy-input-bg, #222);
|
background-color: var(--comfy-input-bg, #222);
|
||||||
@@ -2020,7 +2020,7 @@ to { transform: rotate(360deg);
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
|
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
|
||||||
.text-input.vue-dom-mode[data-v-7fb96eed] {
|
.text-input.vue-dom-mode[data-v-b3b00fdd] {
|
||||||
background-color: var(--color-charcoal-400, #313235);
|
background-color: var(--color-charcoal-400, #313235);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
|
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
|
||||||
@@ -2029,12 +2029,12 @@ to { transform: rotate(360deg);
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.text-input[data-v-7fb96eed]:focus {
|
.text-input[data-v-b3b00fdd]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clear button styles */
|
/* Clear button styles */
|
||||||
.clear-button[data-v-7fb96eed] {
|
.clear-button[data-v-b3b00fdd] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
bottom: 6px; /* Changed from top to bottom */
|
bottom: 6px; /* Changed from top to bottom */
|
||||||
@@ -2057,31 +2057,31 @@ to { transform: rotate(360deg);
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Show clear button when hovering over input wrapper */
|
/* Show clear button when hovering over input wrapper */
|
||||||
.input-wrapper:hover .clear-button[data-v-7fb96eed] {
|
.input-wrapper:hover .clear-button[data-v-b3b00fdd] {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.clear-button[data-v-7fb96eed]:hover {
|
.clear-button[data-v-b3b00fdd]:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: rgba(255, 100, 100, 0.8);
|
background: rgba(255, 100, 100, 0.8);
|
||||||
}
|
}
|
||||||
.clear-button svg[data-v-7fb96eed] {
|
.clear-button svg[data-v-b3b00fdd] {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vue DOM mode adjustments for clear button */
|
/* Vue DOM mode adjustments for clear button */
|
||||||
.text-input.vue-dom-mode ~ .clear-button[data-v-7fb96eed] {
|
.text-input.vue-dom-mode ~ .clear-button[data-v-b3b00fdd] {
|
||||||
right: 8px;
|
right: 8px;
|
||||||
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
|
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: rgba(107, 114, 128, 0.6);
|
background: rgba(107, 114, 128, 0.6);
|
||||||
}
|
}
|
||||||
.text-input.vue-dom-mode ~ .clear-button[data-v-7fb96eed]:hover {
|
.text-input.vue-dom-mode ~ .clear-button[data-v-b3b00fdd]:hover {
|
||||||
background: oklch(62% 0.18 25);
|
background: oklch(62% 0.18 25);
|
||||||
}
|
}
|
||||||
.text-input.vue-dom-mode ~ .clear-button svg[data-v-7fb96eed] {
|
.text-input.vue-dom-mode ~ .clear-button svg[data-v-b3b00fdd] {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}`));
|
}`));
|
||||||
@@ -14239,6 +14239,36 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|||||||
props.widget.callback(textareaRef.value.value);
|
props.widget.callback(textareaRef.value.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const onWheel = (event) => {
|
||||||
|
const textarea = textareaRef.value;
|
||||||
|
if (!textarea) return;
|
||||||
|
const canScrollY = textarea.scrollHeight > textarea.clientHeight;
|
||||||
|
const deltaX = event.deltaX;
|
||||||
|
const deltaY = event.deltaY;
|
||||||
|
const isHorizontal = Math.abs(deltaX) > Math.abs(deltaY);
|
||||||
|
const app2 = window.app;
|
||||||
|
if (!app2 || !app2.canvas || typeof app2.canvas.processMouseWheel !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
app2.canvas.processMouseWheel(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isHorizontal) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
app2.canvas.processMouseWheel(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canScrollY) {
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
app2.canvas.processMouseWheel(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
const onExternalValueChange = (event) => {
|
const onExternalValueChange = (event) => {
|
||||||
updateHasTextState();
|
updateHasTextState();
|
||||||
};
|
};
|
||||||
@@ -14297,7 +14327,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|||||||
placeholder: __props.placeholder,
|
placeholder: __props.placeholder,
|
||||||
spellcheck: __props.spellcheck ?? false,
|
spellcheck: __props.spellcheck ?? false,
|
||||||
class: normalizeClass(["text-input", { "vue-dom-mode": isVueDomMode.value }]),
|
class: normalizeClass(["text-input", { "vue-dom-mode": isVueDomMode.value }]),
|
||||||
onInput
|
onInput,
|
||||||
|
onWheel
|
||||||
}, null, 42, _hoisted_3),
|
}, null, 42, _hoisted_3),
|
||||||
showClearButton.value ? (openBlock(), createElementBlock("button", {
|
showClearButton.value ? (openBlock(), createElementBlock("button", {
|
||||||
key: 0,
|
key: 0,
|
||||||
@@ -14331,7 +14362,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-7fb96eed"]]);
|
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-b3b00fdd"]]);
|
||||||
const LORA_PROVIDER_NODE_TYPES$1 = [
|
const LORA_PROVIDER_NODE_TYPES$1 = [
|
||||||
"Lora Stacker (LoraManager)",
|
"Lora Stacker (LoraManager)",
|
||||||
"Lora Randomizer (LoraManager)",
|
"Lora Randomizer (LoraManager)",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user