1217 lines
66 KiB
JavaScript
1217 lines
66 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
const LocalLoraGalleryNode = {
|
|
name: "LocalLoraGallery",
|
|
isLoading: false,
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
|
|
async getLoras(filter_tag = "", mode = "OR", folder = "", page = 1, selected_loras = [], name_filter = "") {
|
|
this.isLoading = true;
|
|
try {
|
|
let url = `/localloragallery/get_loras?filter_tag=${encodeURIComponent(filter_tag)}&mode=${mode}&folder=${encodeURIComponent(folder)}&page=${page}&name_filter=${encodeURIComponent(name_filter)}`;
|
|
selected_loras.forEach(lora => {
|
|
url += `&selected_loras=${encodeURIComponent(lora)}`;
|
|
});
|
|
const response = await api.fetchApi(url);
|
|
const data = await response.json();
|
|
this.totalPages = data.total_pages || 1;
|
|
this.currentPage = data.current_page || 1;
|
|
return data;
|
|
} catch (error) {
|
|
console.error("LocalLoraGallery: Error fetching LoRAs:", error);
|
|
return { loras: [], folders: [], total_pages: 1, current_page: 1 };
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
|
|
async updateMetadata(lora_name, data) {
|
|
try {
|
|
const body = { lora_name, ...data };
|
|
await api.fetchApi("/localloragallery/update_metadata", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
} catch(e) {
|
|
console.error("LocalLoraGallery: Failed to update metadata", e);
|
|
}
|
|
},
|
|
|
|
async setUiState(nodeId, galleryId, state) {
|
|
try {
|
|
await api.fetchApi("/localloragallery/set_ui_state", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
node_id: nodeId,
|
|
gallery_id: galleryId,
|
|
state: state
|
|
}),
|
|
});
|
|
} catch(e) {
|
|
console.error("LocalLoraGallery: Failed to set UI state", e);
|
|
}
|
|
},
|
|
|
|
setup(nodeType, nodeData) {
|
|
const onConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function () {
|
|
onConfigure?.apply(this, arguments);
|
|
this.isDeserialized = true;
|
|
};
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const result = onNodeCreated?.apply(this, arguments);
|
|
|
|
if (!this.properties || !this.properties.lora_gallery_unique_id) {
|
|
if (!this.properties) { this.properties = {}; }
|
|
this.properties.lora_gallery_unique_id = "lora-gallery-" + Math.random().toString(36).substring(2, 11);
|
|
}
|
|
|
|
const galleryIdWidget = this.addWidget(
|
|
"hidden_text",
|
|
"lora_gallery_unique_id_widget",
|
|
this.properties.lora_gallery_unique_id,
|
|
() => {},
|
|
{}
|
|
);
|
|
|
|
galleryIdWidget.serializeValue = () => {
|
|
return this.properties.lora_gallery_unique_id;
|
|
};
|
|
|
|
galleryIdWidget.draw = function(ctx, node, widget_width, y, widget_height) {};
|
|
galleryIdWidget.computeSize = function(width) {
|
|
return [0, 0];
|
|
}
|
|
|
|
const HEADER_HEIGHT = 110;
|
|
const MIN_NODE_WIDTH = 600;
|
|
|
|
this.size = [700, 600];
|
|
this.loraData = [];
|
|
this.availableLoras = [];
|
|
this.isModelOnly = nodeData.name.includes("ModelOnly") || nodeData.name.includes("Stacker");
|
|
this.selectedCardsForEditing = new Set();
|
|
|
|
const node_instance = this;
|
|
const selectionWidget = this.addWidget(
|
|
"hidden_text",
|
|
"selection_data",
|
|
this.properties.selection_data || "[]",
|
|
() => {},
|
|
{ multiline: true }
|
|
);
|
|
selectionWidget.serializeValue = () => {
|
|
return node_instance.properties["selection_data"] || "[]";
|
|
};
|
|
selectionWidget.draw = function(ctx, node, widget_width, y, widget_height) {};
|
|
selectionWidget.computeSize = function(width) { return [0, 0]; };
|
|
|
|
const widgetContainer = document.createElement("div");
|
|
widgetContainer.className = "locallora-container-wrapper";
|
|
|
|
widgetContainer.dataset.captureWheel = "true";
|
|
widgetContainer.addEventListener("wheel", (e) => e.stopPropagation());
|
|
|
|
this.addDOMWidget("gallery", "div", widgetContainer, {});
|
|
|
|
const uniqueId = `locallora-gallery-${this.id}`;
|
|
|
|
widgetContainer.innerHTML = `
|
|
<style>
|
|
/* --- General Styles --- */
|
|
#${uniqueId} .locallora-container { display: flex; flex-direction: column; height: 100%; font-family: sans-serif; overflow: hidden; }
|
|
#${uniqueId} .locallora-selected-list { flex-shrink: 0; padding: 5px; background-color: #22222200; max-height: 100%; }
|
|
#${uniqueId} .locallora-controls { display: flex; flex-direction: column; padding: 5px; gap: 5px; flex-shrink: 0; }
|
|
#${uniqueId} .locallora-controls-row { display: flex; gap: 10px; align-items: center; }
|
|
#${uniqueId} .locallora-gallery { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; background-color: #1a1a1a; padding: 5px; display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; align-content: start; }
|
|
|
|
/* --- Lora Card --- */
|
|
#${uniqueId} .locallora-lora-card { cursor: pointer; border: 3px solid transparent; border-radius: 8px; background-color: var(--comfy-input-bg); transition: border-color 0.2s; display: flex; flex-direction: column; position: relative; }
|
|
#${uniqueId} .locallora-lora-card.selected-edit { border-color: #FFD700; box-shadow: 0 0 10px #FFD700; }
|
|
#${uniqueId} .locallora-lora-card.selected-flow { border-color: #00FFC9; }
|
|
#${uniqueId} .locallora-media-container { width: 100%; height: 150px; background-color: #111; border-top-left-radius: 5px; border-top-right-radius: 5px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
|
|
#${uniqueId} .locallora-media-container img, #${uniqueId} .locallora-media-container video { width: 100%; height: 100%; object-fit: cover; }
|
|
#${uniqueId} .locallora-lora-card-info { padding: 4px; flex-grow: 1; display: flex; flex-direction: column; }
|
|
#${uniqueId} .locallora-lora-card p { font-size: 11px; margin: 0; word-break: break-all; text-align: center; color: var(--node-text-color); }
|
|
#${uniqueId} .lora-card-triggers { font-size: 10px; color: #a5a5a5; padding: 2px 4px; margin-top: 2px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-height: 14px; }
|
|
#${uniqueId} .lora-card-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-top: auto; padding-top: 4px; }
|
|
#${uniqueId} .lora-card-tags .tag { background-color: #006699; color: #fff; padding: 1px 4px; font-size: 10px; border-radius: 3px; cursor: pointer; }
|
|
#${uniqueId} .lora-card-tags .tag:hover { background-color: #0088CC; }
|
|
|
|
/* --- Card Buttons (Edit, Link, Sync) --- */
|
|
#${uniqueId} .card-btn {
|
|
position: absolute; width: 22px; height: 22px; background-color: rgba(0,0,0,0.5);
|
|
color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
font-size: 14px; cursor: pointer; transition: all 0.2s; opacity: 0; text-decoration: none;
|
|
z-index: 10;
|
|
}
|
|
#${uniqueId} .locallora-lora-card:hover .card-btn { opacity: 1; }
|
|
#${uniqueId} .card-btn:hover { background-color: rgba(0,0,0,0.8); }
|
|
|
|
#${uniqueId} .lora-card-link-btn { top: 4px; right: 4px; }
|
|
#${uniqueId} .edit-tags-btn { bottom: 4px; right: 4px; font-size: 12px; }
|
|
#${uniqueId} .sync-civitai-btn { top: 4px; left: 4px; font-size: 12px; }
|
|
#${uniqueId} .sync-civitai-btn.loading { animation: spin 1s linear infinite; pointer-events: none; background-color: #4a90e2; }
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
/* --- Scrollbar --- */
|
|
#${uniqueId} .locallora-gallery::-webkit-scrollbar { width: 8px; }
|
|
#${uniqueId} .locallora-gallery::-webkit-scrollbar-track { background: #2a2a2a; border-radius: 4px; }
|
|
#${uniqueId} .locallora-gallery::-webkit-scrollbar-thumb { background-color: #555; border-radius: 4px; }
|
|
#${uniqueId} .locallora-gallery::-webkit-scrollbar-thumb:hover { background-color: #777; }
|
|
|
|
/* --- Metadata Editor --- */
|
|
#${uniqueId} .locallora-metadata-editor { display: none; flex-direction: column; gap: 5px; }
|
|
#${uniqueId} .locallora-metadata-editor.visible { display: flex; }
|
|
#${uniqueId} .tag-editor-list .tag .remove-tag { margin-left: 4px; color: #fdd; cursor: pointer; font-weight: bold; }
|
|
|
|
/* --- Selected List Item --- */
|
|
#${uniqueId} .locallora-lora-item { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; cursor: grab; user-select: none; }
|
|
#${uniqueId} .locallora-lora-item .lora-name { flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; }
|
|
#${uniqueId} .locallora-lora-item input[type=number] { width: 60px; background-color: #333; border: 1px solid #555; border-radius: 4px; color: #ccc; }
|
|
#${uniqueId} .locallora-lora-item .lora-label { font-size: 10px; color: var(--node-text-color); }
|
|
#${uniqueId} .locallora-lora-item .remove-lora-btn { background: #555; color: #fff; border: none; border-radius: 10%; text-align: center; cursor: pointer; margin-left: auto; flex-shrink: 0; }
|
|
#${uniqueId} .locallora-lora-item .remove-lora-btn:hover { background: #ff4444; }
|
|
#${uniqueId} .locallora-lora-item.dragging { opacity: 0.5; background: #555; }
|
|
#${uniqueId} .locallora-lora-item.drag-over { border-top: 2px solid #4A90E2; }
|
|
|
|
/* --- Controls & Inputs --- */
|
|
#${uniqueId} .locallora-controls-row input[type=text], #${uniqueId} .locallora-controls-row select { background: #222; color: #ccc; border: 1px solid #555; padding: 4px; border-radius: 4px; }
|
|
#${uniqueId} .tag-filter-mode-btn { padding: 4px 8px; background-color: #555; color: #fff; border: 1px solid #666; border-radius: 4px; cursor: pointer; flex-shrink: 0; }
|
|
#${uniqueId} .tag-filter-mode-btn:hover { background-color: #666; }
|
|
#${uniqueId} .tag-filter-input-wrapper { display: flex; flex-grow: 1; position: relative; align-items: center; }
|
|
#${uniqueId} .tag-filter-input-wrapper input { flex-grow: 1; }
|
|
#${uniqueId} .clear-tag-filter-btn { background: none; border: none; color: #ccc; cursor: pointer; position: absolute; right: 4px; top: 50%; transform: translateY(-50%); display: none; }
|
|
#${uniqueId} .tag-filter-input-wrapper input:not(:placeholder-shown) + .clear-tag-filter-btn { display: block; }
|
|
|
|
/* --- Multi-Select Dropdown --- */
|
|
#${uniqueId} .locallora-multiselect-tag { position: relative; flex-grow: 1; }
|
|
#${uniqueId} .locallora-multiselect-tag-display { background-color: #333; color: #ccc; border: 1px solid #555; border-radius: 4px; padding: 4px; font-size: 10px; height: 23px; cursor: pointer; display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
|
|
#${uniqueId} .locallora-multiselect-arrow { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease-in-out; font-size: 10px; pointer-events: none; }
|
|
#${uniqueId} .locallora-multiselect-arrow.open { transform: translateY(-50%) rotate(180deg); }
|
|
#${uniqueId} .locallora-multiselect-tag-dropdown { display: none; position: absolute; top: 100%; left: 0; right: 0; background-color: #222; border: 1px solid #555; border-top: none; max-height: 200px; overflow-y: auto; z-index: 10; }
|
|
#${uniqueId} .locallora-multiselect-tag-dropdown label { display: block; padding: 0px 0px; cursor: pointer; font-size: 12px; color: #ccc; }
|
|
#${uniqueId} .locallora-multiselect-tag-dropdown label:hover { background-color: #444; }
|
|
|
|
/* --- Presets --- */
|
|
#${uniqueId} .locallora-preset-container { position: relative; display: inline-block; margin-bottom: 0px; }
|
|
#${uniqueId} .preset-dropdown { display: none; position: absolute; background-color: #222; min-width: 160px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 10; border: 1px solid #555; right: 0; }
|
|
#${uniqueId} .preset-dropdown a { color: #ccc; padding: 8px 12px; text-decoration: none; display: flex; justify-content: space-between; align-items: center; font-size: 12px; }
|
|
#${uniqueId} .preset-dropdown a:hover { background-color: #444; }
|
|
#${uniqueId} .delete-preset-btn { color: #ff6666; cursor: pointer; font-weight: bold; padding-left: 10px; }
|
|
#${uniqueId} .delete-preset-btn:hover { color: #ff0000; }
|
|
|
|
/* Misc */
|
|
#${uniqueId} .locallora-container.gallery-collapsed .locallora-gallery { display: none; }
|
|
</style>
|
|
<div id="${uniqueId}" style="height: 100%;">
|
|
<div class="locallora-container">
|
|
<div class="locallora-selected-list"></div>
|
|
<div class="locallora-controls">
|
|
<div class="locallora-controls-row">
|
|
<button class="toggle-all-btn">Toggle All</button>
|
|
<input type="text" class="search-input" placeholder="Filter by Name..." style="flex-grow: 1;">
|
|
<button class="save-preset-btn" title="Save current stack as preset">Save Preset</button>
|
|
<div class="locallora-preset-container">
|
|
<button class="load-preset-btn">Load Preset ▼</button>
|
|
<div class="preset-dropdown"></div>
|
|
</div>
|
|
<button class="clear-all-btn" title="Clear all selected LoRAs">Clear All</button>
|
|
</div>
|
|
|
|
<div class="locallora-metadata-editor">
|
|
<div class="locallora-controls-row">
|
|
<label style="font-size:12px;">Edit Tags (<span class="selected-count">0</span>):</label>
|
|
<div class="tag-editor-list lora-card-tags" style="flex-grow:1;"></div>
|
|
<input type="text" class="tag-editor-input" placeholder="Add tag..." style="width: 100px;">
|
|
</div>
|
|
<div class="locallora-controls-row trigger-editor-row" style="display:none;">
|
|
<label style="font-size:12px;">Triggers:</label>
|
|
<input type="text" class="trigger-editor-input" placeholder="Enter trigger words..." style="flex-grow: 1;">
|
|
</div>
|
|
<div class="locallora-controls-row url-editor-row" style="display:none;">
|
|
<label style="font-size:12px;">URL:</label>
|
|
<input type="text" class="url-editor-input" placeholder="Enter download URL..." style="flex-grow: 1;">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="locallora-controls-row">
|
|
<button class="tag-filter-mode-btn" title="Click to switch filter mode">OR</button>
|
|
<div class="tag-filter-input-wrapper">
|
|
<input type="text" class="tag-filter-input" placeholder="Filter by Tag...">
|
|
<button class="clear-tag-filter-btn" title="Clear Tag Filter">✖</button>
|
|
</div>
|
|
<div class="locallora-multiselect-tag">
|
|
<div class="locallora-multiselect-tag-display">
|
|
Select Tags
|
|
<span class="locallora-multiselect-arrow">▼</span>
|
|
</div>
|
|
<div class="locallora-multiselect-tag-dropdown"></div>
|
|
</div>
|
|
<select class="folder-filter-select" style="max-width: 150px;">
|
|
<option value="">All Folders</option>
|
|
</select>
|
|
<button class="toggle-gallery-btn" title="Toggle Gallery" style="margin-left: auto; flex-shrink: 0;">Hide Gallery</button>
|
|
</div>
|
|
</div>
|
|
<div class="locallora-gallery"><p>Loading LoRAs...</p></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const mainContainer = widgetContainer.querySelector(".locallora-container");
|
|
const selectedListEl = widgetContainer.querySelector(".locallora-selected-list");
|
|
const galleryEl = widgetContainer.querySelector(".locallora-gallery");
|
|
const searchInput = widgetContainer.querySelector(".search-input");
|
|
const metadataEditor = widgetContainer.querySelector(".locallora-metadata-editor");
|
|
const tagEditorList = widgetContainer.querySelector(".tag-editor-list");
|
|
const tagEditorInput = widgetContainer.querySelector(".tag-editor-input");
|
|
const triggerEditorRow = widgetContainer.querySelector(".trigger-editor-row");
|
|
const triggerEditorInput = widgetContainer.querySelector(".trigger-editor-input");
|
|
const urlEditorRow = widgetContainer.querySelector(".url-editor-row");
|
|
const urlEditorInput = widgetContainer.querySelector(".url-editor-input");
|
|
const tagFilterInput = widgetContainer.querySelector(".tag-filter-input");
|
|
const multiSelectTagContainer = widgetContainer.querySelector(".locallora-multiselect-tag");
|
|
const multiSelectTagDisplay = multiSelectTagContainer.querySelector(".locallora-multiselect-tag-display");
|
|
const multiSelectTagDropdown = multiSelectTagContainer.querySelector(".locallora-multiselect-tag-dropdown");
|
|
const tagFilterModeBtn = widgetContainer.querySelector(".tag-filter-mode-btn");
|
|
const toggleGalleryBtn = widgetContainer.querySelector(".toggle-gallery-btn");
|
|
const selectedCountEl = widgetContainer.querySelector(".selected-count");
|
|
const clearTagFilterBtn = widgetContainer.querySelector(".clear-tag-filter-btn");
|
|
const folderFilterSelect = widgetContainer.querySelector(".folder-filter-select");
|
|
const savePresetBtn = widgetContainer.querySelector(".save-preset-btn");
|
|
const loadPresetBtn = widgetContainer.querySelector(".load-preset-btn");
|
|
const presetDropdown = widgetContainer.querySelector(".preset-dropdown");
|
|
|
|
const saveStateAndFetch = () => {
|
|
const stateToSave = {
|
|
filter_tag: tagFilterInput.value,
|
|
filter_mode: tagFilterModeBtn.textContent,
|
|
filter_folder: folderFilterSelect.value
|
|
};
|
|
LocalLoraGalleryNode.setUiState(this.id, this.properties.lora_gallery_unique_id, stateToSave);
|
|
fetchAndRender(false);
|
|
};
|
|
|
|
const updatePresetButtonText = (presetName = null) => {
|
|
loadPresetBtn.textContent = presetName ? `Preset: ${presetName} ▼` : "Load Preset ▼";
|
|
loadPresetBtn.title = presetName ? `Current Preset: ${presetName}` : "Load a saved preset";
|
|
};
|
|
|
|
galleryEl.addEventListener('scroll', () => {
|
|
if (this.isLoading || this.currentPage >= this.totalPages) return;
|
|
const { scrollTop, scrollHeight, clientHeight } = galleryEl;
|
|
if (scrollHeight - scrollTop - clientHeight < 400) {
|
|
fetchAndRender(true);
|
|
}
|
|
});
|
|
|
|
const updateSelection = () => {
|
|
const serializableData = this.loraData.map(({ element, ...rest }) => rest);
|
|
const selectionJson = JSON.stringify(serializableData);
|
|
|
|
this.setProperty("selection_data", selectionJson);
|
|
const widget = this.widgets.find(w => w.name === "selection_data");
|
|
if (widget) widget.value = selectionJson;
|
|
|
|
LocalLoraGalleryNode.setUiState(this.id, this.properties.lora_gallery_unique_id, {
|
|
is_collapsed: mainContainer.classList.contains("gallery-collapsed"),
|
|
lora_stack: serializableData
|
|
});
|
|
};
|
|
|
|
let draggedIndex = -1;
|
|
|
|
const renderSelectedList = () => {
|
|
selectedListEl.innerHTML = "";
|
|
this.loraData.forEach((item, index) => {
|
|
const el = document.createElement("div");
|
|
el.className = "locallora-lora-item";
|
|
el.draggable = true;
|
|
el.dataset.index = index;
|
|
|
|
const toggle = document.createElement("input");
|
|
toggle.type = "checkbox";
|
|
toggle.checked = item.on;
|
|
toggle.addEventListener("change", (e) => { this.loraData[index].on = e.target.checked; updateSelection(); });
|
|
|
|
const nameLabel = document.createElement("span");
|
|
nameLabel.className = "lora-name";
|
|
nameLabel.textContent = item.lora;
|
|
nameLabel.title = item.lora;
|
|
|
|
const trigLabel = document.createElement("span");
|
|
trigLabel.className = "lora-label";
|
|
trigLabel.textContent = "Trig";
|
|
trigLabel.title = "Enable/Disable Trigger Words";
|
|
trigLabel.style.marginLeft = "5px";
|
|
trigLabel.style.cursor = "help";
|
|
|
|
const trigInput = document.createElement("input");
|
|
trigInput.type = "checkbox";
|
|
trigInput.checked = item.use_trigger !== false;
|
|
trigInput.title = "Toggle Trigger Words";
|
|
trigInput.style.marginRight = "5px";
|
|
trigInput.addEventListener("change", (e) => {
|
|
this.loraData[index].use_trigger = e.target.checked;
|
|
updateSelection();
|
|
});
|
|
|
|
const strengthModelLabel = document.createElement("span");
|
|
strengthModelLabel.className = "lora-label";
|
|
strengthModelLabel.textContent = "Model";
|
|
|
|
const strengthModelInput = document.createElement("input");
|
|
strengthModelInput.type = "number";
|
|
strengthModelInput.value = item.strength;
|
|
strengthModelInput.min = -2.0; strengthModelInput.max = 2.0; strengthModelInput.step = 0.05;
|
|
strengthModelInput.addEventListener("change", (e) => { this.loraData[index].strength = parseFloat(e.target.value); updateSelection(); });
|
|
|
|
el.appendChild(toggle);
|
|
el.appendChild(nameLabel);
|
|
el.appendChild(trigLabel);
|
|
el.appendChild(trigInput);
|
|
el.appendChild(strengthModelLabel);
|
|
el.appendChild(strengthModelInput);
|
|
|
|
if (!this.isModelOnly) {
|
|
const strengthClipLabel = document.createElement("span");
|
|
strengthClipLabel.className = "lora-label";
|
|
strengthClipLabel.textContent = "CLIP";
|
|
const strengthClipInput = document.createElement("input");
|
|
strengthClipInput.type = "number";
|
|
strengthClipInput.value = item.strength_clip;
|
|
strengthClipInput.min = -2.0; strengthClipInput.max = 2.0; strengthClipInput.step = 0.05;
|
|
strengthClipInput.addEventListener("change", (e) => { this.loraData[index].strength_clip = parseFloat(e.target.value); updateSelection(); });
|
|
el.appendChild(strengthClipLabel);
|
|
el.appendChild(strengthClipInput);
|
|
}
|
|
|
|
const removeBtn = document.createElement("button");
|
|
removeBtn.className = "remove-lora-btn";
|
|
removeBtn.textContent = "✖";
|
|
removeBtn.title = "Remove LoRA";
|
|
removeBtn.addEventListener("click", () => {
|
|
this.loraData.splice(index, 1);
|
|
renderSelectedList();
|
|
updateSelection();
|
|
if (mainContainer.classList.contains("gallery-collapsed")) {
|
|
setTimeout(() => {
|
|
const controlsEl = widgetContainer.querySelector(".locallora-controls");
|
|
const contentHeight = selectedListEl.scrollHeight + controlsEl.offsetHeight;
|
|
this.size[1] = contentHeight + HEADER_HEIGHT;
|
|
this.setDirtyCanvas(true, true);
|
|
}, 0);
|
|
}
|
|
fetchAndRender(false);
|
|
updatePresetButtonText(null);
|
|
});
|
|
el.appendChild(removeBtn);
|
|
|
|
el.addEventListener('dragstart', (e) => {
|
|
draggedIndex = parseInt(e.currentTarget.dataset.index);
|
|
e.currentTarget.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
});
|
|
el.addEventListener('dragend', (e) => e.currentTarget.classList.remove('dragging'));
|
|
el.addEventListener('dragover', (e) => e.preventDefault());
|
|
el.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
const targetIndex = parseInt(e.currentTarget.dataset.index);
|
|
if (draggedIndex !== targetIndex) {
|
|
const [movedItem] = this.loraData.splice(draggedIndex, 1);
|
|
this.loraData.splice(targetIndex, 0, movedItem);
|
|
updateSelection();
|
|
renderSelectedList();
|
|
}
|
|
});
|
|
|
|
selectedListEl.appendChild(el);
|
|
});
|
|
};
|
|
|
|
const syncWithCivitai = async (loraName, card) => {
|
|
const syncBtn = card.querySelector('.sync-civitai-btn');
|
|
syncBtn.textContent = '🔄';
|
|
syncBtn.classList.add('loading');
|
|
|
|
try {
|
|
const response = await api.fetchApi("/localloragallery/sync_civitai", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ lora_name: loraName }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'ok' && result.metadata) {
|
|
const { preview_url, preview_type, trigger_words, download_url, tags } = result.metadata;
|
|
|
|
const loraInDataSource = this.availableLoras.find(l => l.name === loraName);
|
|
if (loraInDataSource) {
|
|
loraInDataSource.preview_url = preview_url || '';
|
|
loraInDataSource.preview_type = preview_type || 'none';
|
|
loraInDataSource.trigger_words = trigger_words || '';
|
|
loraInDataSource.download_url = download_url || '';
|
|
loraInDataSource.tags = tags || [];
|
|
}
|
|
|
|
const mediaContainer = card.querySelector('.locallora-media-container');
|
|
if (preview_type === 'video' && preview_url) {
|
|
mediaContainer.innerHTML = `<video muted loop playsinline src="${preview_url}"></video>`;
|
|
const video = mediaContainer.querySelector('video');
|
|
card.addEventListener('mouseenter', () => video.play().catch(e => {}));
|
|
card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; });
|
|
} else if (preview_type === 'image' && preview_url) {
|
|
mediaContainer.innerHTML = `<img src="${preview_url}">`;
|
|
} else {
|
|
const empty_lora_image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
mediaContainer.innerHTML = `<img src="${empty_lora_image}">`;
|
|
}
|
|
|
|
const triggerEl = card.querySelector('.lora-card-triggers');
|
|
if(triggerEl) {
|
|
triggerEl.textContent = trigger_words || 'No triggers';
|
|
triggerEl.title = trigger_words || '';
|
|
}
|
|
card.dataset.triggerWords = trigger_words || '';
|
|
card.dataset.downloadUrl = download_url || '';
|
|
card.dataset.tags = (tags || []).join(',');
|
|
|
|
const oldLinkBtn = card.querySelector('.lora-card-link-btn');
|
|
if(oldLinkBtn) oldLinkBtn.remove();
|
|
if(download_url){
|
|
const linkBtn = document.createElement('a');
|
|
linkBtn.href = download_url;
|
|
linkBtn.target = '_blank';
|
|
linkBtn.className = 'card-btn lora-card-link-btn';
|
|
linkBtn.title = 'Open download page';
|
|
linkBtn.innerHTML = '🔗';
|
|
linkBtn.addEventListener('click', e => e.stopPropagation());
|
|
card.prepend(linkBtn);
|
|
}
|
|
|
|
renderCardTags(card);
|
|
await loadAllTags();
|
|
} else {
|
|
throw new Error(result.message || 'Sync failed');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("LocalLoraGallery: Failed to sync with Civitai:", error);
|
|
syncBtn.textContent = '❌';
|
|
setTimeout(() => syncBtn.textContent = '☁️', 2000);
|
|
} finally {
|
|
syncBtn.classList.remove('loading');
|
|
if(syncBtn.textContent !== '❌') syncBtn.textContent = '☁️';
|
|
}
|
|
};
|
|
|
|
const renderGallery = (append = false) => {
|
|
if (!append) galleryEl.innerHTML = "";
|
|
const nameFilter = searchInput.value.toLowerCase();
|
|
const lorasToRender = this.availableLoras.filter(lora => lora.name.toLowerCase().includes(nameFilter));
|
|
const existingCardNames = new Set(Array.from(galleryEl.querySelectorAll('.locallora-lora-card')).map(c => c.dataset.loraName));
|
|
|
|
lorasToRender.forEach(lora => {
|
|
if (append && existingCardNames.has(lora.name)) return;
|
|
|
|
const card = document.createElement("div");
|
|
card.className = "locallora-lora-card";
|
|
card.dataset.loraName = lora.name;
|
|
card.dataset.tags = lora.tags.join(',');
|
|
card.dataset.triggerWords = lora.trigger_words;
|
|
card.dataset.downloadUrl = lora.download_url;
|
|
card.title = lora.name;
|
|
|
|
let mediaHTML = '';
|
|
const empty_lora_image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
const previewUrl = lora.preview_url;
|
|
|
|
if (lora.preview_type === 'video' && previewUrl) {
|
|
mediaHTML = `<video muted loop playsinline src="${previewUrl}"></video>`;
|
|
} else {
|
|
mediaHTML = `<img src="${previewUrl || empty_lora_image}" loading="lazy">`;
|
|
}
|
|
|
|
const linkBtnHTML = lora.download_url ? `<a href="${lora.download_url}" target="_blank" class="card-btn lora-card-link-btn" title="Open download page">🔗</a>` : '';
|
|
|
|
card.innerHTML = `
|
|
<div class="card-btn sync-civitai-btn" title="Sync with Civitai">☁️</div>
|
|
${linkBtnHTML}
|
|
<div class="locallora-media-container">${mediaHTML}</div>
|
|
<div class="locallora-lora-card-info">
|
|
<p>${lora.name}</p>
|
|
<div class="lora-card-triggers" title="${lora.trigger_words}">${lora.trigger_words || 'No triggers'}</div>
|
|
<div class="lora-card-tags"></div>
|
|
</div>
|
|
<div class="card-btn edit-tags-btn">✏️</div>
|
|
`;
|
|
|
|
if (lora.preview_type !== 'video') {
|
|
card.querySelector("img").onerror = (e) => { e.target.src = empty_lora_image; };
|
|
}
|
|
galleryEl.appendChild(card);
|
|
|
|
card.querySelector(".lora-card-link-btn")?.addEventListener("click", e => e.stopPropagation());
|
|
card.querySelector(".sync-civitai-btn").addEventListener("click", e => {
|
|
e.stopPropagation();
|
|
syncWithCivitai(lora.name, card);
|
|
});
|
|
|
|
if (this.loraData.some(item => item.lora === lora.name)) card.classList.add("selected-flow");
|
|
if (this.selectedCardsForEditing.has(card)) card.classList.add("selected-edit");
|
|
|
|
renderCardTags(card);
|
|
|
|
if (lora.preview_type === 'video') {
|
|
const video = card.querySelector('video');
|
|
if (video) {
|
|
card.addEventListener('mouseenter', () => video.play().catch(e => {}));
|
|
card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; });
|
|
}
|
|
}
|
|
|
|
card.addEventListener("click", () => {
|
|
const loraName = card.dataset.loraName;
|
|
const existingIndex = this.loraData.findIndex(item => item.lora === loraName);
|
|
if (existingIndex > -1) {
|
|
this.loraData.splice(existingIndex, 1);
|
|
} else {
|
|
this.loraData.push({ on: true, lora: loraName, strength: 1.0, strength_clip: 1.0, use_trigger: true });
|
|
}
|
|
renderSelectedList();
|
|
updateSelection();
|
|
updatePresetButtonText(null);
|
|
|
|
card.classList.toggle("selected-flow");
|
|
});
|
|
|
|
const editBtn = card.querySelector(".edit-tags-btn");
|
|
editBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
|
|
if (e.ctrlKey) {
|
|
if (this.selectedCardsForEditing.has(card)) {
|
|
this.selectedCardsForEditing.delete(card);
|
|
card.classList.remove("selected-edit");
|
|
} else {
|
|
this.selectedCardsForEditing.add(card);
|
|
card.classList.add("selected-edit");
|
|
}
|
|
} else {
|
|
if (this.selectedCardsForEditing.has(card) && this.selectedCardsForEditing.size === 1) {
|
|
this.selectedCardsForEditing.clear();
|
|
card.classList.remove("selected-edit");
|
|
} else {
|
|
document.querySelectorAll(`#${uniqueId} .locallora-lora-card.selected-edit`).forEach(c => c.classList.remove("selected-edit"));
|
|
this.selectedCardsForEditing.clear();
|
|
|
|
this.selectedCardsForEditing.add(card);
|
|
card.classList.add("selected-edit");
|
|
}
|
|
}
|
|
|
|
renderMetadataEditor();
|
|
});
|
|
});
|
|
};
|
|
|
|
const debounce = (func, delay) => {
|
|
let timeoutId;
|
|
return (...args) => {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(() => {
|
|
func.apply(this, args);
|
|
}, delay);
|
|
};
|
|
};
|
|
|
|
const fetchAndRender = async (append = false) => {
|
|
if (this.isLoading) return;
|
|
const pageToFetch = append ? this.currentPage + 1 : 1;
|
|
if (append && pageToFetch > this.totalPages) return;
|
|
|
|
const currentSearchTerm = searchInput ? searchInput.value.trim() : "";
|
|
|
|
const { loras, folders } = await LocalLoraGalleryNode.getLoras.call(
|
|
this,
|
|
tagFilterInput.value,
|
|
tagFilterModeBtn.textContent,
|
|
folderFilterSelect.value,
|
|
pageToFetch,
|
|
this.loraData.map(item => item.lora),
|
|
currentSearchTerm
|
|
);
|
|
|
|
if (append) {
|
|
const existingNames = new Set(this.availableLoras.map(l => l.name));
|
|
this.availableLoras.push(...(loras || []).filter(l => !existingNames.has(l.name)));
|
|
} else {
|
|
this.availableLoras = loras || [];
|
|
if (!foldersRendered && folders && folders.length > 0) renderFolders(folders);
|
|
galleryEl.scrollTop = 0;
|
|
}
|
|
renderGallery(append);
|
|
};
|
|
|
|
const handleTagSelectionChange = () => {
|
|
const selectedTags = Array.from(multiSelectTagDropdown.querySelectorAll('input:checked')).map(cb => cb.value);
|
|
tagFilterInput.value = selectedTags.join(',');
|
|
saveStateAndFetch();
|
|
};
|
|
|
|
const loadAllTags = async () => {
|
|
try {
|
|
const response = await api.fetchApi("/localloragallery/get_all_tags");
|
|
const data = await response.json();
|
|
multiSelectTagDropdown.innerHTML = '';
|
|
if (data.tags) {
|
|
data.tags.forEach(tag => {
|
|
const label = document.createElement('label');
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.value = tag;
|
|
checkbox.addEventListener('change', handleTagSelectionChange);
|
|
label.append(checkbox, ` ${tag}`);
|
|
multiSelectTagDropdown.appendChild(label);
|
|
});
|
|
}
|
|
} catch(e) { console.error("LocalLoraGallery: Failed to load all tags:", e); }
|
|
};
|
|
|
|
let foldersRendered = false;
|
|
const renderFolders = (folders) => {
|
|
if (foldersRendered) return;
|
|
const currentVal = folderFilterSelect.value;
|
|
folderFilterSelect.innerHTML = `<option value="">All Folders</option>`;
|
|
folders.forEach(folder => {
|
|
const option = document.createElement('option');
|
|
option.value = folder;
|
|
option.textContent = folder === "." ? "Root" : folder.replaceAll('\\', '/');
|
|
folderFilterSelect.appendChild(option);
|
|
});
|
|
folderFilterSelect.value = currentVal;
|
|
if (folders.length > 0) foldersRendered = true;
|
|
};
|
|
|
|
const renderPresets = (presets) => {
|
|
presetDropdown.innerHTML = '';
|
|
for (const name in presets) {
|
|
const presetLink = document.createElement('a');
|
|
presetLink.href = '#';
|
|
presetLink.dataset.presetName = name;
|
|
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.textContent = name;
|
|
presetLink.appendChild(nameSpan);
|
|
|
|
const deleteBtn = document.createElement('span');
|
|
deleteBtn.className = 'delete-preset-btn';
|
|
deleteBtn.textContent = '✖';
|
|
deleteBtn.title = 'Delete Preset';
|
|
deleteBtn.onclick = async (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
if (confirm(`Are you sure you want to delete preset "${name}"?`)) {
|
|
const res = await api.fetchApi("/localloragallery/delete_preset", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
const data = await res.json();
|
|
renderPresets(data.presets);
|
|
}
|
|
};
|
|
presetLink.appendChild(deleteBtn);
|
|
|
|
presetLink.onclick = (e) => {
|
|
e.preventDefault();
|
|
this.loraData = JSON.parse(JSON.stringify(presets[name]));
|
|
|
|
renderSelectedList();
|
|
|
|
setTimeout(() => {
|
|
const HEADER_HEIGHT = 90;
|
|
const controlsEl = widgetContainer.querySelector(".locallora-controls");
|
|
const requiredTopHeight = selectedListEl.scrollHeight + controlsEl.offsetHeight;
|
|
|
|
if (mainContainer.classList.contains("gallery-collapsed")) {
|
|
this.size[1] = requiredTopHeight + HEADER_HEIGHT;
|
|
} else {
|
|
const galleryHeight = galleryEl.clientHeight;
|
|
const newTotalHeight = requiredTopHeight + galleryHeight + HEADER_HEIGHT;
|
|
|
|
if (newTotalHeight > this.size[1]) {
|
|
this.size[1] = newTotalHeight;
|
|
this.expandedHeight = newTotalHeight;
|
|
}
|
|
}
|
|
this.setDirtyCanvas(true, true);
|
|
}, 0);
|
|
|
|
updateSelection();
|
|
fetchAndRender(false);
|
|
presetDropdown.style.display = 'none';
|
|
|
|
updatePresetButtonText(name);
|
|
};
|
|
presetDropdown.appendChild(presetLink);
|
|
}
|
|
};
|
|
|
|
const loadPresets = async () => {
|
|
try {
|
|
const res = await api.fetchApi("/localloragallery/get_presets");
|
|
const presets = await res.json();
|
|
renderPresets(presets);
|
|
} catch (e) { console.error("LocalLoraGallery: Failed to load presets", e); }
|
|
};
|
|
|
|
const renderMetadataEditor = () => {
|
|
selectedCountEl.textContent = this.selectedCardsForEditing.size;
|
|
|
|
if (this.selectedCardsForEditing.size === 0) {
|
|
metadataEditor.classList.remove("visible");
|
|
return;
|
|
}
|
|
|
|
tagEditorList.innerHTML = "";
|
|
const allTags = Array.from(this.selectedCardsForEditing).map(card => card.dataset.tags ? card.dataset.tags.split(',').filter(Boolean) : []);
|
|
const commonTags = allTags.reduce((a, b) => a.filter(c => b.includes(c)));
|
|
|
|
commonTags.forEach(tag => {
|
|
const tagEl = document.createElement("span");
|
|
tagEl.className = "tag";
|
|
tagEl.textContent = tag;
|
|
const removeEl = document.createElement("span");
|
|
removeEl.className = "remove-tag";
|
|
removeEl.textContent = "ⓧ";
|
|
removeEl.onclick = async (e) => {
|
|
e.stopPropagation();
|
|
const updatePromises = Array.from(this.selectedCardsForEditing).map(async (card) => {
|
|
const loraName = card.dataset.loraName;
|
|
const tags = card.dataset.tags ? card.dataset.tags.split(',').filter(Boolean) : [];
|
|
const newTags = tags.filter(t => t !== tag);
|
|
|
|
await LocalLoraGalleryNode.updateMetadata(loraName, { tags: newTags });
|
|
|
|
card.dataset.tags = newTags.join(',');
|
|
const loraInDataSource = this.availableLoras.find(lora => lora.name === loraName);
|
|
if (loraInDataSource) loraInDataSource.tags = newTags;
|
|
renderCardTags(card);
|
|
});
|
|
await Promise.all(updatePromises);
|
|
await loadAllTags();
|
|
renderMetadataEditor();
|
|
};
|
|
tagEl.appendChild(removeEl);
|
|
tagEditorList.appendChild(tagEl);
|
|
});
|
|
|
|
if (this.selectedCardsForEditing.size === 1) {
|
|
const selectedCard = Array.from(this.selectedCardsForEditing)[0];
|
|
triggerEditorInput.value = selectedCard.dataset.triggerWords || "";
|
|
triggerEditorRow.style.display = "flex";
|
|
urlEditorInput.value = selectedCard.dataset.downloadUrl || "";
|
|
urlEditorRow.style.display = "flex";
|
|
} else {
|
|
triggerEditorRow.style.display = "none";
|
|
urlEditorRow.style.display = "none";
|
|
}
|
|
|
|
metadataEditor.classList.add("visible");
|
|
};
|
|
|
|
const renderCardTags = (card) => {
|
|
const tagContainer = card.querySelector(".lora-card-tags");
|
|
tagContainer.innerHTML = "";
|
|
const tags = card.dataset.tags ? card.dataset.tags.split(',').filter(Boolean) : [];
|
|
tags.forEach(tag => {
|
|
const tagEl = document.createElement("span");
|
|
tagEl.className = "tag";
|
|
tagEl.textContent = tag;
|
|
tagEl.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
tagFilterInput.value = tag;
|
|
fetchAndRender();
|
|
});
|
|
tagContainer.appendChild(tagEl);
|
|
});
|
|
};
|
|
|
|
this.initializeNode = async () => {
|
|
let initialState = {
|
|
is_collapsed: false,
|
|
lora_stack: [],
|
|
filter_tag: "",
|
|
filter_mode: "OR",
|
|
filter_folder: ""
|
|
};
|
|
try {
|
|
const res = await api.fetchApi(`/localloragallery/get_ui_state?node_id=${this.id}&gallery_id=${this.properties.lora_gallery_unique_id}`);
|
|
const loadedState = await res.json();
|
|
initialState = { ...initialState, ...loadedState };
|
|
} catch(e) {
|
|
console.error("LocalLoraGallery: Failed to get initial UI state.", e);
|
|
}
|
|
|
|
if (this.isDeserialized) {
|
|
const widget = this.widgets.find(w => w.name === "selection_data");
|
|
if (widget && widget.value) {
|
|
try {
|
|
this.loraData = JSON.parse(widget.value);
|
|
} catch (e) {
|
|
this.loraData = [];
|
|
}
|
|
} else {
|
|
this.loraData = [];
|
|
}
|
|
} else {
|
|
this.loraData = initialState.lora_stack || [];
|
|
}
|
|
|
|
const selectionJson = JSON.stringify(this.loraData);
|
|
this.setProperty("selection_data", selectionJson);
|
|
const widget = this.widgets.find(w => w.name === "selection_data");
|
|
if (widget) widget.value = selectionJson;
|
|
|
|
tagFilterInput.value = initialState.filter_tag;
|
|
if (initialState.filter_mode === "AND") {
|
|
tagFilterModeBtn.textContent = "AND";
|
|
tagFilterModeBtn.style.backgroundColor = "#D97706";
|
|
} else {
|
|
tagFilterModeBtn.textContent = "OR";
|
|
tagFilterModeBtn.style.backgroundColor = "#555";
|
|
}
|
|
|
|
await loadAllTags();
|
|
await loadPresets();
|
|
await fetchAndRender();
|
|
|
|
let needs_refetch = false;
|
|
if (initialState.filter_folder && folderFilterSelect.querySelector(`option[value="${initialState.filter_folder}"]`)) {
|
|
if (folderFilterSelect.value !== initialState.filter_folder) {
|
|
folderFilterSelect.value = initialState.filter_folder;
|
|
needs_refetch = true;
|
|
}
|
|
}
|
|
|
|
const selectedTags = new Set(initialState.filter_tag.split(',').filter(Boolean));
|
|
multiSelectTagDropdown.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
|
cb.checked = selectedTags.has(cb.value);
|
|
});
|
|
|
|
renderSelectedList();
|
|
|
|
if (initialState.is_collapsed) {
|
|
setTimeout(() => {
|
|
const controlsEl = widgetContainer.querySelector(".locallora-controls");
|
|
if (!controlsEl) return;
|
|
const contentHeight = selectedListEl.scrollHeight + controlsEl.offsetHeight;
|
|
this.size[1] = contentHeight + HEADER_HEIGHT;
|
|
mainContainer.classList.add("gallery-collapsed");
|
|
toggleGalleryBtn.textContent = "Show Gallery";
|
|
this.setDirtyCanvas(true, true);
|
|
}, 0);
|
|
}
|
|
|
|
if (needs_refetch) {
|
|
await fetchAndRender();
|
|
}
|
|
};
|
|
|
|
this.expandedHeight = this.size[1];
|
|
|
|
const bindEventListeners = () => {
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") {
|
|
if (this.selectedCardsForEditing.size > 0) {
|
|
document.querySelectorAll(`#${uniqueId} .locallora-lora-card.selected-edit`).forEach(c => c.classList.remove("selected-edit"));
|
|
this.selectedCardsForEditing.clear();
|
|
renderMetadataEditor();
|
|
}
|
|
}
|
|
});
|
|
|
|
urlEditorInput.addEventListener("keydown", async (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (this.selectedCardsForEditing.size !== 1) return;
|
|
|
|
const selectedCard = Array.from(this.selectedCardsForEditing)[0];
|
|
const loraName = selectedCard.dataset.loraName;
|
|
const newUrl = urlEditorInput.value.trim();
|
|
|
|
await LocalLoraGalleryNode.updateMetadata(loraName, { download_url: newUrl });
|
|
|
|
selectedCard.dataset.downloadUrl = newUrl;
|
|
const loraInDataSource = this.availableLoras.find(l => l.name === loraName);
|
|
if (loraInDataSource) loraInDataSource.download_url = newUrl;
|
|
|
|
let linkBtn = selectedCard.querySelector('.lora-card-link-btn');
|
|
if (newUrl) {
|
|
if (!linkBtn) {
|
|
linkBtn = document.createElement('a');
|
|
linkBtn.className = 'card-btn lora-card-link-btn';
|
|
linkBtn.title = 'Open download page';
|
|
linkBtn.innerHTML = '🔗';
|
|
linkBtn.target = '_blank';
|
|
linkBtn.addEventListener("click", (e) => e.stopPropagation());
|
|
selectedCard.prepend(linkBtn);
|
|
}
|
|
linkBtn.href = newUrl;
|
|
} else if (linkBtn) {
|
|
linkBtn.remove();
|
|
}
|
|
|
|
const originalColor = urlEditorInput.style.backgroundColor;
|
|
urlEditorInput.style.backgroundColor = "#2a5";
|
|
setTimeout(() => { urlEditorInput.style.backgroundColor = ""; }, 500);
|
|
}
|
|
});
|
|
|
|
triggerEditorInput.addEventListener("keydown", async (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (this.selectedCardsForEditing.size !== 1) return;
|
|
|
|
const selectedCard = Array.from(this.selectedCardsForEditing)[0];
|
|
const loraName = selectedCard.dataset.loraName;
|
|
const newTriggers = triggerEditorInput.value.trim();
|
|
|
|
await LocalLoraGalleryNode.updateMetadata(loraName, { trigger_words: newTriggers });
|
|
|
|
selectedCard.dataset.triggerWords = newTriggers;
|
|
const loraInDataSource = this.availableLoras.find(l => l.name === loraName);
|
|
if (loraInDataSource) loraInDataSource.trigger_words = newTriggers;
|
|
|
|
const triggerDisplayEl = selectedCard.querySelector('.lora-card-triggers');
|
|
if(triggerDisplayEl) {
|
|
triggerDisplayEl.textContent = newTriggers || 'No triggers';
|
|
triggerDisplayEl.title = newTriggers;
|
|
}
|
|
|
|
const originalColor = triggerEditorInput.style.backgroundColor;
|
|
triggerEditorInput.style.backgroundColor = "#2a5";
|
|
setTimeout(() => { triggerEditorInput.style.backgroundColor = ""; }, 500);
|
|
}
|
|
});
|
|
|
|
tagEditorInput.addEventListener("keydown", async (e) => {
|
|
if (e.key === 'Enter' && tagEditorInput.value.trim()) {
|
|
e.preventDefault();
|
|
const newTag = tagEditorInput.value.trim();
|
|
if (newTag) {
|
|
const updatePromises = Array.from(this.selectedCardsForEditing).map(async (card) => {
|
|
const loraName = card.dataset.loraName;
|
|
const tags = card.dataset.tags ? card.dataset.tags.split(',').filter(Boolean) : [];
|
|
|
|
if (!tags.includes(newTag)) {
|
|
tags.push(newTag);
|
|
await LocalLoraGalleryNode.updateMetadata(loraName, { tags: tags });
|
|
card.dataset.tags = tags.join(',');
|
|
const loraInDataSource = this.availableLoras.find(lora => lora.name === loraName);
|
|
if (loraInDataSource) loraInDataSource.tags = [...tags];
|
|
renderCardTags(card);
|
|
}
|
|
});
|
|
await Promise.all(updatePromises);
|
|
await loadAllTags();
|
|
renderMetadataEditor();
|
|
e.target.value = "";
|
|
}
|
|
}
|
|
});
|
|
|
|
clearTagFilterBtn.addEventListener("click", () => {
|
|
tagFilterInput.value = "";
|
|
multiSelectTagDropdown.querySelectorAll('input:checked').forEach(cb => cb.checked = false);
|
|
saveStateAndFetch();
|
|
});
|
|
|
|
widgetContainer.querySelector(".clear-all-btn").addEventListener("click", () => {
|
|
this.loraData = [];
|
|
if (mainContainer.classList.contains("gallery-collapsed")) {
|
|
setTimeout(() => {
|
|
const controlsEl = widgetContainer.querySelector(".locallora-controls");
|
|
if (!controlsEl) return;
|
|
const contentHeight = selectedListEl.scrollHeight + controlsEl.offsetHeight;
|
|
this.size[1] = contentHeight + HEADER_HEIGHT;
|
|
this.setDirtyCanvas(true, true);
|
|
}, 0);
|
|
}
|
|
fetchAndRender(false);
|
|
renderSelectedList();
|
|
updateSelection();
|
|
updatePresetButtonText(null);
|
|
});
|
|
|
|
folderFilterSelect.addEventListener("change", saveStateAndFetch);
|
|
|
|
tagFilterModeBtn.addEventListener("click", () => {
|
|
if (tagFilterModeBtn.textContent === "OR") {
|
|
tagFilterModeBtn.textContent = "AND";
|
|
tagFilterModeBtn.style.backgroundColor = "#D97706";
|
|
} else {
|
|
tagFilterModeBtn.textContent = "OR";
|
|
tagFilterModeBtn.style.backgroundColor = "#555";
|
|
}
|
|
saveStateAndFetch();
|
|
});
|
|
|
|
savePresetBtn.addEventListener("click", async () => {
|
|
const presetName = prompt("Enter a name for this preset:", "");
|
|
if (presetName && this.loraData.length > 0) {
|
|
const res = await api.fetchApi("/localloragallery/save_preset", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: presetName, data: this.loraData }),
|
|
});
|
|
const data = await res.json();
|
|
renderPresets(data.presets);
|
|
}
|
|
});
|
|
|
|
loadPresetBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
presetDropdown.style.display = presetDropdown.style.display === 'block' ? 'none' : 'block';
|
|
});
|
|
|
|
toggleGalleryBtn.addEventListener("click", () => {
|
|
const isCollapsing = !mainContainer.classList.contains("gallery-collapsed");
|
|
|
|
if (isCollapsing) {
|
|
this.expandedHeight = this.size[1];
|
|
const controlsEl = widgetContainer.querySelector(".locallora-controls");
|
|
const contentHeight = selectedListEl.scrollHeight + controlsEl.offsetHeight;
|
|
this.size[1] = contentHeight + HEADER_HEIGHT;
|
|
mainContainer.classList.add("gallery-collapsed");
|
|
toggleGalleryBtn.textContent = "Show Gallery";
|
|
} else {
|
|
this.size[1] = this.expandedHeight;
|
|
mainContainer.classList.remove("gallery-collapsed");
|
|
toggleGalleryBtn.textContent = "Hide Gallery";
|
|
}
|
|
|
|
LocalLoraGalleryNode.setUiState(this.id, this.properties.lora_gallery_unique_id, {
|
|
is_collapsed: isCollapsing,
|
|
lora_stack: this.loraData.map(({ element, ...rest }) => rest)
|
|
});
|
|
app.graph.setDirty(true);
|
|
});
|
|
|
|
widgetContainer.querySelector(".toggle-all-btn").addEventListener("click", () => {
|
|
const allOn = this.loraData.every(item => item.on);
|
|
this.loraData.forEach(item => item.on = !allOn);
|
|
renderSelectedList();
|
|
updateSelection();
|
|
});
|
|
|
|
const debouncedServerSearch = debounce(() => {
|
|
this.currentPage = 1;
|
|
fetchAndRender(false);
|
|
}, 300);
|
|
|
|
searchInput.addEventListener("input", () => {
|
|
renderGallery(false);
|
|
debouncedServerSearch();
|
|
});
|
|
|
|
tagFilterInput.addEventListener("keydown", (e) => { if(e.key === 'Enter') saveStateAndFetch(); });
|
|
|
|
const arrow = multiSelectTagContainer.querySelector('.locallora-multiselect-arrow');
|
|
multiSelectTagDisplay.addEventListener('click', () => {
|
|
const isVisible = multiSelectTagDropdown.style.display === 'block';
|
|
multiSelectTagDropdown.style.display = isVisible ? 'none' : 'block';
|
|
arrow.classList.toggle('open', !isVisible);
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!multiSelectTagContainer.contains(e.target)) {
|
|
multiSelectTagDropdown.style.display = 'none';
|
|
arrow.classList.remove('open');
|
|
}
|
|
if (presetDropdown && !loadPresetBtn.contains(e.target) && !presetDropdown.contains(e.target)) {
|
|
presetDropdown.style.display = 'none';
|
|
}
|
|
});
|
|
};
|
|
|
|
const fitHeight = () => {
|
|
if (!widgetContainer) return;
|
|
|
|
const size = node_instance.size;
|
|
|
|
let topOffset = widgetContainer.offsetTop;
|
|
|
|
if (topOffset < 20) {
|
|
const baseHeader = 60;
|
|
const extraInputHeight = node_instance.isModelOnly ? 0 : 20;
|
|
|
|
topOffset += (baseHeader + extraInputHeight);
|
|
}
|
|
|
|
const bottomPadding = 20;
|
|
|
|
let targetHeight = size[1] - topOffset - bottomPadding;
|
|
|
|
if (targetHeight < 0) targetHeight = 0;
|
|
|
|
window.requestAnimationFrame(() => {
|
|
widgetContainer.style.height = targetHeight + "px";
|
|
widgetContainer.style.width = "100%";
|
|
});
|
|
};
|
|
|
|
this.onResize = function(size) {
|
|
const controlsEl = widgetContainer.querySelector(".locallora-controls");
|
|
let calculatedMinHeight = selectedListEl.scrollHeight + (controlsEl?.offsetHeight || 0) + HEADER_HEIGHT;
|
|
|
|
if (!mainContainer.classList.contains("gallery-collapsed")) {
|
|
calculatedMinHeight += 300;
|
|
this.expandedHeight = Math.max(size[1], calculatedMinHeight);
|
|
}
|
|
|
|
if (size[1] < calculatedMinHeight) size[1] = calculatedMinHeight;
|
|
if (size[0] < MIN_NODE_WIDTH) size[0] = MIN_NODE_WIDTH;
|
|
|
|
fitHeight();
|
|
};
|
|
|
|
bindEventListeners();
|
|
|
|
setTimeout(async () => {
|
|
await this.initializeNode();
|
|
fitHeight();
|
|
requestAnimationFrame(() => fitHeight());
|
|
}, 1);
|
|
|
|
return result;
|
|
};
|
|
}
|
|
};
|
|
|
|
app.registerExtension({
|
|
name: "LocalLoraGallery.GalleryUI",
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (
|
|
nodeData.name === "LocalLoraGallery" ||
|
|
nodeData.name === "LocalLoraGalleryModelOnly" ||
|
|
nodeData.name === "LocalLoraGalleryStacker"
|
|
) {
|
|
LocalLoraGalleryNode.setup(nodeType, nodeData);
|
|
}
|
|
},
|
|
});
|
|
|