Files

2867 lines
104 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { $el } from "../../../scripts/ui.js";
import { ComfyDialog } from "../../../scripts/ui/dialog.js";
import { api } from "../../../scripts/api.js";
import { ComfyAsyncDialog } from "../../../scripts/ui/components/asyncDialog.js";
class SliderSidebar {
constructor(app) {
console.log("Initializing SliderSidebar");
this.app = app;
this.sliderHooks = [];
this.customCategories = {};
this.defaultCategories = ["Favorites", "Custom", "Face", "Body", "Image"];
this.searchInput = this.createSearchInput();
this.randomizeButton = this.createRandomizeButton();
this.hideInactiveCheckbox = this.createHideInactiveCheckbox();
this.accordion = $el("div.slider-accordion");
this.controlsRow = $el("div.controls-row", [
this.randomizeButton,
this.hideInactiveCheckbox
]);
this.element = $el("div.slider-sidebar", [
$el("h3", "Slider Sidebar"),
this.searchInput,
this.controlsRow,
this.accordion
]);
this.loadSliderData(); // Load slider data from all JSON files
this.addSliderButton = this.createAddSliderButton();
this.element.appendChild(this.addSliderButton);
this.initializeCategories();
this.categoryOrder = [];
this.loadCategoryOrder();
this.favorites = new Set();
this.loadFavorites();
this.processSliderHooks();
this.fixReverseSliders = app.ui.settings.getSettingValue("Slider Sidebar.Sliders.unreversed", false);
this.nsfwEnabled = app.ui.settings.getSettingValue("Slider Sidebar.General.nsfw", false);
this.hideMissing = app.ui.settings.getSettingValue("Slider Sidebar.Visual.hideMissing", false);
this.sliderStep = app.ui.settings.getSettingValue("Slider Sidebar.Sliders.sliderStep", 0.25);
this.popupMaxWidth = app.ui.settings.getSettingValue("Slider Sidebar.Visual.popupMaxWidth", 70);
this.showPills = app.ui.settings.getSettingValue("Slider Sidebar.Visual.showPills", true);
this.smartWiring = app.ui.settings.getSettingValue("Slider Sidebar.General.smartWires", true);
this.modelFilter = app.ui.settings.getSettingValue("Slider Sidebar.Visual.showModels", "All");
this.ignoreSliderLimits = app.ui.settings.getSettingValue("Slider Sidebar.Sliders.sliderLimits", false);
this.showFavorites = app.ui.settings.getSettingValue("Slider Sidebar.Visual.showFavorites", true);
this.hideInactive = false;
this.availableLoras = [];
this.loadAvailableLoras();
this.setupCanvasDropHandling();
this.apiKey = app.ui.settings.getSettingValue("Slider Sidebar.General.apiKey", "");
this.imageCache = new Map();
this.loraDetails = [];
this.loadCustomSliders();
this.updatePopupMaxWidth(this.popupMaxWidth);
this.updatePillVisibility(this.showPills);
this.updateButton = null;
this.checkForUpdates();
this.debug = true; // Set this to true to enable debug logging
// cramming in css, proabbly doing it wrong lol
this.addCustomStyles();
}
addCustomStyles() {
const style = document.createElement('style');
style.textContent = `
.slider-sidebar h3 {
display: flex;
justify-content: space-around;
}
.slider-sidebar-search {
display: block;
width: 90%;
margin: 0 auto 15px;
padding: 10px;
border: 1px solid var(--border-color-light, #4B5563);
border-radius: 5px;
background-color: var(--input-background-color, #2a2e33);
color: var(--input-text-color, #E2E8F0);
font-size: 14px;
transition: all 0.3s ease;
}
.slider-sidebar-search:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(75, 85, 99, 0.5);
}
.slider-sidebar-search::placeholder {
color: var(--input-text-color-muted, #6B7280);
}
.accordion-section {
transition: all 0.3s ease;
position: relative;
}
.accordion-section:not(:last-child)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: transparent;
transition: background-color 0.3s ease;
}
.accordion-section:not(:last-child):hover::after {
background-color: var(--border-color-light, #4B5563);
}
.accordion-section.dragging {
opacity: 0.5;
border: 2px dashed #4a6da7;
}
.accordion-section:not(.dragging) {
cursor: grab;
}
.accordion-section:not(.dragging):active {
cursor: grabbing;
}
.accordion-section.new {
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.accordion-section.new.show {
opacity: 1;
transform: translateY(0);
}
.accordion-header {
display: flex;
justify-content: space-between; /* This pushes the content to the edges */
align-items: center;
font-size: 1.1rem;
font-weight: bold;
padding: 10px;
background-color: rgba(60, 60, 60, 0.5);
border-bottom: 2px solid var(--border-color-light, #4B5563);
cursor: grab;
transition: background-color 0.3s ease;
position: relative;
z-index: 1;
user-select: none;
}
.accordion-header i {
font-size: 1.2rem;
margin-left: auto;
transition: transform 0.3s ease;
cursor: auto;
}
.accordion-header:hover {
background-color: rgba(80, 80, 80, 0.5);
}
.accordion-header:active {
cursor: grabbing;
}
.drop-indicator {
position: absolute;
height: 2px;
background-color: #4a6da7; /* Changed to visible color */
box-shadow: 0 0 5px rgba(74, 109, 167, 0.7); /* Add a glow effect */
pointer-events: none;
z-index: 1000;
}
.accordion-content {
padding: 10px 0;
}
.slider-container {
position: relative;
padding: 0.5rem 0.25rem;
border-bottom: 1px solid var(--border-color-light, #444);
cursor: auto;
}
.slider-container span {
user-select: none;
}
.slider-container.disabled {
position: relative;
cursor: move;
}
.slider-container.disabled::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.35);
pointer-events: none;
z-index: 1;
border-radius: inherit;
}
.slider-container.disabled input[type="range"],
.slider-container.disabled input[type="checkbox"] {
pointer-events: none;
}
.slider-container.disabled input[type="range"] {
background: #ccc;
filter: grayscale(100%);
}
.slider-container.disabled input[type="range"]::-webkit-slider-thumb {
background: #999;
filter: grayscale(100%);
}
.slider-container.disabled input[type="range"]::-moz-range-thumb {
background: #999;
filter: grayscale(100%);
}
.slider-container.disabled input[type="checkbox"] {
filter: grayscale(100%);
}
.pill {
font-size: 0.65rem;
padding: 0.1rem 0.3rem;
border-radius: 12px;
display: inline-flex;
opacity: var(--slider-pill-opacity);
align-items: center;
gap: 0.2rem;
margin-left: 0.25rem;
white-space: nowrap;
height: 1.2rem;
border: 1px solid var(--border-color-light, #4B5563);
}
.author-pill {
background-color: var(--input-background-color, #3B82F6);
color: var(--input-text-color, white);
}
.slider-second-row {
flex-wrap: wrap;
}
.chkpt-pill {
background-color: #FFA500;
color: black;
margin-top: 1px;
margin-bottom: 1px;
}
.pill i {
font-size: 0.65rem;
}
.pill-text {
overflow: hidden;
text-overflow: ellipsis;
}
.slider-first-row {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 0.25rem;
flex-wrap: wrap;
}
.slider-first-row label {
font-size: 0.85rem;
margin-right: auto;
display: flex;
align-items: center;
}
.label-container {
display: flex;
align-items: center;
gap: 7px;
cursor: pointer;
}
.label-text {
font-size: 1.0rem;
}
.slider-second-row {
display: flex;
align-items: center;
}
.slider-second-row input[type="range"] {
flex-grow: 1;
margin-right: 0.5rem;
height: 4px;
-webkit-appearance: none;
background: var(--input-background-color, #cfe0fc);
outline: none;
border-radius: 2px;
}
.slider-second-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #3B82F6;
cursor: pointer;
}
.slider-second-row input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #3B82F6;
cursor: pointer;
}
.slider-value {
font-size: 0.75rem;
min-width: 3rem;
text-align: center;
border: 1px solid var(--border-color-light, #4B5563);
border-radius: 4px;
padding: 0.1rem;
margin-right: 0.5rem;
}
.slider-container.missing-lora {
background-color: rgba(255, 0, 0, 0.1);
cursor: not-allowed;
}
.slider-container .download-button {
position: relative;
z-index: 2;
margin-left: 8px;
padding: 4px 8px;
opacity: 1 !important; /* Ensure full opacity */
cursor: pointer;
background-color: #4a6da7;
color: #fff;
border: none;
border-radius: 4px;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.slider-container .download-button:hover {
background-color: #5a7db7; /* Slightly lighter blue on hover */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.slider-container .download-button:active {
background-color: #3a5d97; /* Darker blue when clicked */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(1px);
}
.slider-container .download-button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(74, 109, 167, 0.5); /* Focus ring */
}
.slider-container .downloading .download-button {
cursor: progress;
}
.model-info-popup {
position: absolute;
z-index: 1000;
background-color: #222;
border: 1px solid #444;
padding: 10px;
border-radius: 5px;
color: #fff;
object-fit: contain;
max-width: var(--slider-popup-max-width);
max-height: var(--slider-popup-max-height);
overflow: clip;
transition: opacity 0.3s ease, transform 0.3s ease;
opacity: 0;
transform: translateY(-10px);
}
.model-info-popup img {
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
border-radius: 5px;
margin-bottom: 10px;
}
.model-info-popup.show {
opacity: 1;
transform: translateY(0);
}
.add-slider-button {
display: block;
margin: 15px auto;
padding: 10px 20px;
font-size: 16px;
width: 88%;
color: white;
background-color: #4a6da7;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s;
}
.add-slider-button:hover {
background-color: #5a7db7;
}
.lora-file-list {
max-height: 300px;
z-index:1000;
overflow-y: auto;
list-style-type: none;
padding: 0;
}
.lora-file-list li {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
.lora-file-list li:hover {
background-color: #f0f0f0;
}
.lora-file-list li.selected {
background-color: #e0e0e0;
font-weight: bold;
}
/* Dialog Container Styles */
.comfy-dialog.comfyui-dialog {
background-color: var(--input-background-color, #2a2e33);
color: var(--input-text-color, #E2E8F0);
padding: 20px;
border: none;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
max-width: 660px; /* Increased from 500px to 660px */
width: 95%; /* Increased from 90% to 95% */
display: flex;
flex-direction: column;
font-family: inherit;
overflow-x: hidden; /* Prevent horizontal scrollbar */
}
/* Dialog Header Styles */
.comfy-dialog.comfyui-dialog h3 {
font-size: 1.2rem;
margin-bottom: 15px;
color: var(--input-text-color, #E2E8F0);
}
/* Search Input within Dialog */
.comfy-dialog.comfyui-dialog input[type="text"] {
display: block;
width: 100%;
margin-bottom: 15px;
padding: 10px;
border: 1px solid var(--border-color-light, #4B5563);
border-radius: 5px;
background-color: var(--input-background-color, #2a2e33);
color: var(--input-text-color, #E2E8F0);
font-size: 14px;
box-sizing: border-box; /* Ensure padding is included in width */
}
.comfy-dialog.comfyui-dialog input[type="text"]:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(75, 85, 99, 0.5);
}
.comfy-dialog.comfyui-dialog input[type="text"]::placeholder {
color: var(--input-text-color-muted, #6B7280);
}
/* LoRA File List within Dialog */
.comfy-dialog.comfyui-dialog .lora-file-list {
max-height: 300px;
z-index: 1000;
overflow-y: auto;
overflow-x: hidden; /* Prevent horizontal scrollbar */
list-style-type: none;
padding: 0;
margin: 0 0 15px 0;
border: 1px solid var(--border-color-light, #4B5563);
border-radius: 5px;
background-color: var(--input-background-color, #2a2e33);
box-sizing: border-box; /* Ensure padding is included in width */
}
.comfy-dialog.comfyui-dialog .lora-file-list li {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #444;
}
.comfy-dialog.comfyui-dialog .lora-file-list li:hover {
background-color: #3a3a3a;
}
.comfy-dialog.comfyui-dialog .lora-file-list li.selected {
background-color: #555;
color: #fff;
font-weight: bold;
}
.comfy-dialog.comfyui-dialog .lora-file-list li:last-child {
border-bottom: none;
}
/* Buttons within Dialog */
.comfy-dialog.comfyui-dialog button.comfyui-button {
padding: 10px 20px;
margin: 5px;
border: none;
border-radius: 5px;
background-color: #4a6da7;
color: #fff;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease, transform 0.1s ease;
box-sizing: border-box; /* Ensure padding is included in width */
}
.comfy-dialog.comfyui-dialog button.comfyui-button:hover {
background-color: #5a7db7;
}
.comfy-dialog.comfyui-dialog button.comfyui-button:active {
transform: scale(0.98);
background-color: #3a5d97;
}
/* Button Container Alignment */
.comfy-dialog.comfyui-dialog .button-container {
display: flex;
justify-content: flex-end;
}
/* Scrollbar Styling (optional) */
.comfy-dialog.comfyui-dialog .lora-file-list::-webkit-scrollbar {
width: 8px;
}
.comfy-dialog.comfyui-dialog .lora-file-list::-webkit-scrollbar-track {
background: var(--input-background-color, #2a2e33);
}
.comfy-dialog.comfyui-dialog .lora-file-list::-webkit-scrollbar-thumb {
background-color: #4B5563;
border-radius: 4px;
}
.category-name {
width: fit-content;
min-width: 0;
cursor: text;
}
.editable-label {
cursor: text;
}
.editable-label input {
font-size: 1.0rem;
border: none;
background: transparent;
color: inherit;
width: 100%;
}
.controls-row {
display: flex;
justify-content: space-between;
align-items: stretch;
margin-bottom: 15px;
}
.randomize-button {
display: block;
margin: 15px auto;
padding: 10px 20px;
font-size: 16px;
width: 48%;
color: white;
background-color: #FFA500;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s;
}
.randomize-button:hover {
background-color: #FFB52E;
}
.hide-inactive-container {
display: flex;
align-items: center;
width: 38%;
border-radius: 5px;
padding: 5px;
}
.hide-inactive-container .label-container {
display: flex;
flex-direction: column;
gap: 0px;
}
.large-checkbox {
width: 24px;
height: 24px;
margin-right: 10px;
cursor: pointer;
}
.label-text {
font-weight: bold;
line-height: 1.2;
}
`;
document.head.appendChild(style);
}
async loadSliderData() {
try {
const response = await fetch('/slider_sidebar/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.sliderHooks = data.sliders;
this.processSliderHooks(); // Make sure to process the hooks after loading
} catch (error) {
console.error("Could not load slider data:", error);
}
}
setupNodeCreatedListener() {
app.graph.addNodeListener("node_created", (node) => {
console.log("Node created:", node.type);
if (node.type === "LoraLoaderModelOnly") {
this.onWorkflowLoaded();
}
});
}
setupNodeListeners() {
const onNodeChange = (node) => {
if (node.comfyClass === "LoraLoaderModelOnly") {
this.updateAllSliders();
app.graph.setDirtyCanvas(true, true);
}
};
app.graph.onNodeAdded = (node) => {
if (node.comfyClass === "LoraLoaderModelOnly") {
this.setupLoraNodeListeners(node);
onNodeChange(node);
}
};
app.graph.onNodeRemoved = onNodeChange;
// Set up listeners for existing LoraLoaderModelOnly nodes
app.graph._nodes.forEach(node => {
if (node.comfyClass === "LoraLoaderModelOnly") {
this.setupLoraNodeListeners(node);
}
});
}
setupLoraNodeListeners(node) {
const loraNameWidget = node.widgets.find(w => w.name === "lora_name");
if (loraNameWidget) {
this.setupLoraNameCallback(loraNameWidget, node);
}
// listen for widget changes
node.onWidgetChanged = (name, value) => {
if (name === "strength_model") {
const hook = this.sliderHooks.find(h => h.loraName === loraNameWidget.value);
if (hook) {
this.updateSliderState(hook, node);
}
}
};
this.setupModePropertyOverride(node, loraNameWidget);
this.debugLog(`Lora node listener set up for node ${node.id}`);
}
setupLoraNameCallback(widget, node) {
const originalCallback = widget.callback;
widget.callback = (value) => {
if (originalCallback) {
originalCallback(value);
}
this.onLoraNameChanged(node, value);
};
}
setupModePropertyOverride(node, loraNameWidget) {
let nodeMode = node.mode;
Object.defineProperty(node, 'mode', {
get: () => nodeMode,
set: (value) => {
this.debugLog(`Mode change detected for node ${node.id}: ${value}`);
nodeMode = value;
this.updateSliderForNode(node, loraNameWidget);
},
enumerable: true,
configurable: true
});
}
updateSliderForNode(node, loraNameWidget) {
if (loraNameWidget) {
const hook = this.sliderHooks.find(h => h.loraName === loraNameWidget.value);
if (hook) {
this.debugLog(`Updating slider for Lora ${hook.name}`);
this.updateSliderState(hook, node);
} else {
this.debugLog(`No matching hook found for Lora: ${loraNameWidget.value}`);
}
} else {
this.debugLog(`No lora_name widget found for node ${node.id}`);
}
}
updateFixReverseSlidersSettings(newValue) {
this.fixReverseSliders = newValue;
this.updateAllSliders();
}
onLoraNameChanged(node, newLoraName) {
console.log(`Lora name changed to: ${newLoraName}`);
this.updateAllSliders();
}
createSearchInput() {
const input = $el("input.slider-sidebar-search", {
type: "text",
placeholder: "Search sliders...",
oninput: () => this.handleSearch(input.value)
});
return input;
}
createAddSliderButton() {
return $el("button.add-slider-button", {
textContent: "Add Slider",
onclick: () => this.showAddSliderDialog()
});
}
///ACTIVE TOGGLE STUFF
createHideInactiveCheckbox() {
const container = $el("div.hide-inactive-container");
const checkbox = $el("input", {
type: "checkbox",
id: "hide-inactive-checkbox",
className: "large-checkbox",
onchange: () => this.toggleInactiveSliders(checkbox.checked)
});
const labelContainer = $el("div.label-container");
const labelTop = $el("div.label-text", { textContent: "Hide" });
const labelBottom = $el("div.label-text", { textContent: "Inactive" });
labelContainer.appendChild(labelTop);
labelContainer.appendChild(labelBottom);
container.appendChild(checkbox);
container.appendChild(labelContainer);
return container;
}
toggleInactiveSliders(hide) {
this.hideInactive = hide;
this.applyHideInactive();
this.updateAllSliders();
}
///////
handleSearch(searchTerm) {
if (!this.accordion) return;
searchTerm = searchTerm.toLowerCase();
this.currentSearchTerm = searchTerm;
const allSections = [...this.accordion.querySelectorAll('.accordion-section')];
allSections.forEach(section => {
const sliders = [...section.querySelectorAll('.slider-container')];
let visibleSliders = 0;
sliders.forEach(slider => {
const sliderName = slider.querySelector('label')?.textContent.toLowerCase() || "";
const sliderCategory = section.querySelector('.accordion-header')?.textContent.toLowerCase() || "";
const authorPill = slider.querySelector('.author-pill .pill-text')?.textContent.toLowerCase() || "";
const chkptPill = slider.querySelector('.chkpt-pill .pill-text')?.textContent.toLowerCase() || "";
if (sliderName.includes(searchTerm) ||
sliderCategory.includes(searchTerm) ||
authorPill.includes(searchTerm) ||
chkptPill.includes(searchTerm)) {
slider.style.display = 'block';
visibleSliders++;
} else {
slider.style.display = 'none';
}
});
// Show/hide the entire section based on whether it has visible sliders
section.style.display = visibleSliders > 0 ? 'block' : 'none';
});
// Apply hide inactive after search
this.applyHideInactive();
}
applyHideInactive() {
if (!this.accordion) return;
const allSliders = [...this.accordion.querySelectorAll('.slider-container')];
allSliders.forEach(slider => {
// Only process sliders that passed the search filter
if (slider.style.display !== 'none') {
const isActive = !slider.classList.contains('disabled');
const isFavorite = slider.querySelector('.pi-heart-fill');
if (this.hideInactive) {
// When hiding inactive sliders
if (!isActive && !(this.showFavorites && isFavorite)) {
slider.style.display = 'none';
} else {
slider.style.display = '';
}
} else {
// When not hiding inactive sliders, show all
slider.style.display = '';
}
}
});
// Hide empty sections
const allSections = [...this.accordion.querySelectorAll('.accordion-section')];
allSections.forEach(section => {
const visibleSliders = [...section.querySelectorAll('.slider-container')].filter(s => s.style.display !== 'none');
section.style.display = visibleSliders.length > 0 ? '' : 'none';
});
}
async loadData() {
try {
const response = await api.fetchApi('/userdata/slider_sidebar_data.json');
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error('Error loading data:', error);
}
return {};
}
async saveData(data) {
try {
const response = await api.fetchApi('/userdata/slider_sidebar_data.json?overwrite=true', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to save data');
}
} catch (error) {
console.error('Error saving data:', error);
}
}
/// LORA CHECKER STUFF
async loadAvailableLoras() {
await this.initializeCategories();
this.availableLoras = await this.fetchAvailableLoras();
this.update(); // Re-render the sliders to reflect the availability
}
async fetchAvailableLoras() {
try {
const response = await api.fetchApi('/loras/list');
if (response.ok) {
const data = await response.json();
// Filter out files without extensions and then extract filenames and normalize them
const loraFiles = data
.filter(item => /\.(safetensors|ckpt|pt)$/i.test(item.filename)) // Ensure valid extensions
.map(item =>
item.filename.replace(/\.(safetensors|ckpt|pt)$/i, '').toLowerCase()
);
return loraFiles;
} else {
console.error('Failed to fetch LoRAs:', response.statusText);
}
} catch (error) {
console.error('Error fetching available LoRAs:', error);
}
return [];
}
updateHideMissingSetting(newVal) {
this.hideMissing = newVal;
this.update();
}
async downloadLoraModel(modelId, container) {
// Indicate that the download is starting
container.classList.add('downloading');
const downloadButton = container.querySelector('.download-button');
if (downloadButton) {
downloadButton.disabled = true;
downloadButton.textContent = 'Downloading...';
}
try {
const response = await api.fetchApi('/loras/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
modelId,
apiKey: this.apiKey || null, // Include the API key if provided
}),
});
if (response.ok) {
// Refresh the list of available LoRAs
await this.loadAvailableLoras();
// Re-render the sliders
this.update();
// Show success toast
app.extensionManager.toast.add({
severity: "success",
summary: "LoRA Downloaded",
detail: "LoRA model was successfully downloaded and is now available.",
life: 3000
});
} else {
const errorData = await response.json();
console.error('Download failed:', errorData.message);
alert(`Failed to download LoRA model: ${errorData.message}`);
}
} catch (error) {
console.error('Error downloading LoRA model:', error);
alert('An error occurred while downloading the LoRA model.');
} finally {
// Remove downloading state
container.classList.remove('downloading');
if (downloadButton) {
downloadButton.disabled = false;
downloadButton.textContent = 'Download';
}
}
}
updateApiKey(newApiKey) {
this.apiKey = newApiKey;
}
///////
createRandomizeButton() {
return $el("button.randomize-button", {
textContent: "Randomize!",
onclick: () => this.randomizeSliders()
});
}
randomizeSliders() {
this.sliderHooks.forEach(hook => {
const sliderContainer = this.accordion.querySelector(`[data-lora-name="${hook.loraName}"]`);
if (sliderContainer && !sliderContainer.classList.contains('disabled')) {
const slider = sliderContainer.querySelector('input[type="range"]');
const valueInput = sliderContainer.querySelector('input[type="number"]');
if (slider && valueInput && !slider.disabled) {
const sliderConfig = this.getSliderConfig(hook);
const randomValue = Math.random() * (sliderConfig.max - sliderConfig.min) + sliderConfig.min;
const roundedValue = Math.round(randomValue / this.sliderStep) * this.sliderStep;
// Update the slider and input value
slider.value = roundedValue;
valueInput.value = roundedValue.toFixed(2);
// Trigger the existing update mechanism
this.handleSliderChange(hook, roundedValue);
}
}
});
}
/// CATEGORY ORDER STUFF
async initializeCategories() {
await this.loadCategoryOrder();
await this.loadFavorites();
// Load custom category names
const data = await this.loadData();
this.categoryNames = data.categoryNames || {};
// Check if categoryOrder is empty
if (!this.categoryOrder || this.categoryOrder.length === 0) {
this.categoryOrder = [...this.defaultCategories];
await this.saveCategoryOrder(); // Save the default order
this.debugLog("Initialized categoryOrder with defaults:", this.categoryOrder);
}
// Proceed to process slider hooks and update the UI
this.processSliderHooks();
this.update(); // Render the UI after initialization
}
async loadCategoryOrder() {
try {
const data = await this.loadData();
if (data.categoryOrder && Array.isArray(data.categoryOrder)) {
this.categoryOrder = data.categoryOrder;
} else {
this.categoryOrder = [];
}
this.debugLog("Loaded category order:", this.categoryOrder);
} catch (error) {
console.error('Error loading category order:', error);
this.categoryOrder = []; // Initialize as empty on error
}
}
async saveCategoryOrder() {
try {
const data = await this.loadData(); // Load existing data
data.categoryOrder = this.categoryOrder; // Update categoryOrder
await this.saveData(data); // Save back to JSON
this.debugLog("Saved category order:", data.categoryOrder);
} catch (error) {
console.error('Error saving category order:', error);
}
}
setupDragAndDropForCategory() {
let draggedElement = null;
let dropIndicator = document.createElement('div');
dropIndicator.className = 'drop-indicator';
const OFFSET = 161; // dirty css hack
this.accordion.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('accordion-header')) {
const category = e.target.dataset.category;
const fixedOrder = app.ui.settings.getSettingValue("Slider Sidebar.Visual.fixedOrder", true);
if (fixedOrder && (category === "Favorites" || category === "Custom")) {
e.preventDefault(); // Prevent dragging of 'Favorites' and 'Custom' when fixedOrder is true
return;
}
draggedElement = e.target.closest('.accordion-section');
e.dataTransfer.setData('text/plain', category);
// Add dragging class for visual feedback
draggedElement.classList.add('dragging');
// Create a custom drag image
const dragImage = e.target.cloneNode(true);
dragImage.style.width = `${e.target.offsetWidth}px`;
dragImage.style.backgroundColor = 'rgba(60, 60, 60, 0.8)';
dragImage.style.position = 'absolute';
dragImage.style.top = '-1000px';
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, 0, 0);
setTimeout(() => {
document.body.removeChild(dragImage);
}, 0);
}
});
this.accordion.addEventListener('dragover', (e) => {
e.preventDefault();
const header = e.target.closest('.accordion-header');
if (header && draggedElement && header.parentNode !== draggedElement) {
const category = header.dataset.category;
const fixedOrder = app.ui.settings.getSettingValue("Slider Sidebar.Visual.fixedOrder", true);
if (fixedOrder && (category === "Favorites" || category === "Custom")) {
// Do not allow dropping before 'Favorites' or 'Custom' when fixedOrder is true
return;
}
const rect = header.getBoundingClientRect();
const accordionRect = this.accordion.getBoundingClientRect();
dropIndicator.style.width = `${rect.width}px`;
dropIndicator.style.left = `${rect.left - accordionRect.left}px`;
dropIndicator.style.top = `${rect.top - accordionRect.top + OFFSET}px`;
this.accordion.appendChild(dropIndicator);
}
});
this.accordion.addEventListener('dragleave', (e) => {
if (!e.target.closest('.accordion-header')) {
dropIndicator.remove();
}
});
this.accordion.addEventListener('dragend', () => {
if (draggedElement) {
draggedElement.classList.remove('dragging'); // Remove dragging class
draggedElement = null;
dropIndicator.remove();
this.updateCategoryOrder();
}
});
this.accordion.addEventListener('drop', (e) => {
e.preventDefault();
dropIndicator.remove();
const header = e.target.closest('.accordion-header');
if (header && draggedElement && header.parentNode !== draggedElement) {
const category = header.dataset.category;
const fixedOrder = app.ui.settings.getSettingValue("Slider Sidebar.Visual.fixedOrder", true);
if (fixedOrder && (category === "Favorites" || category === "Custom")) {
// Do not allow dropping before 'Favorites' or 'Custom' when fixedOrder is true
return;
}
header.parentNode.parentNode.insertBefore(draggedElement, header.parentNode);
this.updateCategoryOrder();
}
});
}
async updateCategoryOrder() {
const sections = this.accordion.querySelectorAll('.accordion-section');
this.categoryOrder = Array.from(sections).map(section => section.dataset.category);
// Debounce the save operation to reduce frequency
if (this.saveTimeout) clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => {
this.saveCategoryOrder();
}, 500);
}
///////
/// ALL TEH FAVORITES STUFF
async loadFavorites() {
try {
const response = await api.fetchApi('/userdata/slider_sidebar_data.json');
if (response.ok) {
const data = await response.json();
this.favorites = new Set(data.favorites);
}
} catch (error) {
console.error('Error loading favorites:', error);
}
}
async saveFavorites() {
try {
const data = await this.loadData();
data.favorites = Array.from(this.favorites);
await this.saveData(data);
} catch (error) {
console.error('Error saving favorites:', error);
}
}
toggleFavorite(hook) {
if (this.favorites.has(hook.name)) {
this.favorites.delete(hook.name);
} else {
this.favorites.add(hook.name);
}
this.saveFavorites();
this.update();
}
///////
processSliderHooks() {
this.categories = {};
this.sliderHooks.forEach(hook => {
if (this.nsfwEnabled || hook.type !== "NSFW") {
// Use original category name for internal structure
const categoryName = hook.type;
if (!this.categories[categoryName]) {
this.categories[categoryName] = [];
}
this.categories[categoryName].push(hook);
}
});
}
getSliderConfig(hook) {
if (this.ignoreSliderLimits) {
return{
min: -20,
max: 20,
defaultValue: 0
}
} else {
if (this.fixReverseSliders && hook.reversed) {
return {
min: -hook.maxValue,
max: -hook.minValue,
defaultValue: -hook.defaultValue
};
} else {
return {
min: hook.minValue,
max: hook.maxValue,
defaultValue: hook.defaultValue
};
}
}
}
getSliderLabel(hook) {
if (this.fixReverseSliders || !hook.reversed) {
return hook.name;
}
return `${hook.name} (Reversed)`;
}
createAuthorPill(author) {
return $el("span.pill.author-pill", [
$el("i.pi.pi-user"),
$el("span.pill-text", { textContent: author })
]);
}
createChkptPills(chkpt) {
const chkpts = chkpt.split(',').map(c => c.trim());
return chkpts.map(c =>
$el("span.pill.chkpt-pill", [
$el("i.pi.pi-verified"),
$el("span.pill-text", { textContent: c })
])
);
}
showModelInfoPopup(hook, iconElement) {
// Check if a popup already exists and remove it
if (this.currentPopup) {
this.currentPopup.remove();
}
// Create the popup container
const popup = $el("div.model-info-popup", { className: "model-info-popup" });
// Create the close button
const closeButton = $el("i", {
className: "pi pi-times",
style: {
position: "absolute",
top: "5px",
right: "5px",
cursor: "pointer",
fontSize: "1.2em"
},
onclick: () => {
popup.remove();
document.removeEventListener("click", closePopup);
this.currentPopup = null;
}
});
// Create the content container
const contentContainer = $el("div", {
style: {
marginTop: "25px" // Add margin to avoid overlap with close button
}
});
const civitAiLink = $el("a", {
href: `https://civitai.com/models/${hook.modelId}`,
textContent: `View Model on CivitAI`,
target: "_blank",
style: {
display: "block",
fontSize: "1em",
marginBottom: "10px",
color: "#4a6da7",
textDecoration: "underline",
}
});
contentContainer.appendChild(civitAiLink);
let currentIndex = 0; // Index to track the current image being shown
let images = []; // Array to hold the fetched images
// Declare updateImage as a placeholder function
let updateImage = () => {};
const prevButton = $el("button", {
textContent: "< Prev Image",
style: {
marginLeft: "10px",
fontSize: "0.9em",
cursor: "pointer",
},
onclick: () => {
if (currentIndex > 0) {
currentIndex--;
updateImage(images[currentIndex]);
}
}
});
const nextButton = $el("button", {
textContent: "Next Image >",
style: {
marginLeft: "10px",
fontSize: "0.9em",
cursor: "pointer",
},
onclick: () => {
if (currentIndex < images.length - 1) {
currentIndex++;
updateImage(images[currentIndex]);
}
}
});
// Add buttons to the same line as the CivitAI link
contentContainer.appendChild(prevButton);
contentContainer.appendChild(nextButton);
// Add enhance keywords if available
if (hook.enhanceKeywords) {
const keywordsText = $el("p", {
style: {
fontSize: "0.9em",
marginBottom: "10px",
lineHeight: "1.4"
}
});
keywordsText.innerHTML = `For stronger results try using <span class="enhance-keywords">${hook.enhanceKeywords}</span> in your prompts.`;
const keywordStyle = $el("style", {
textContent: `
.enhance-keywords {
font-weight: bold;
color: #4a90e2;
background-color: rgba(74, 144, 226, 0.1);
padding: 2px 4px;
border-radius: 3px;
display: inline-block;
margin: 2px 0;
}
`
});
contentContainer.appendChild(keywordStyle);
contentContainer.appendChild(keywordsText);
}
// Add a loading indicator
const loadingText = $el("p", { textContent: "Loading image..." });
contentContainer.appendChild(loadingText);
// Append the close button and content container to popup
popup.appendChild(closeButton);
popup.appendChild(contentContainer);
// Position the popup relative to the icon
const rect = iconElement.getBoundingClientRect();
let top = rect.bottom + window.scrollY;
let left = rect.left + window.scrollX;
// Append the popup to the body first to measure its size later
document.body.appendChild(popup);
// Adjust the position to ensure it's within the viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Apply the initial position
popup.style.top = `${top}px`;
popup.style.left = `${left}px`;
// Class to trigger CSS transitions
setTimeout(() => {
popup.classList.add('show');
}, 0);
// Fetch the image from Civitai
this.fetchModelImages(hook.modelId)
.then((fetchedImages) => {
images = fetchedImages; // Store the fetched images
currentIndex = 0; // Start with the first image
// Remove the loading text
loadingText.remove();
if (images.length > 0) {
// Function to update the displayed image with your existing logic
updateImage = (imageUrl) => {
// Remove the old image if it exists
const oldImage = contentContainer.querySelector("img");
if (oldImage) {
oldImage.remove();
}
// Create the new image element
const image = $el("img", {
src: imageUrl,
classname: "model-info-popup",
onload: function() {
const isWidthLarger = this.width > this.height;
document.documentElement.style.setProperty('--slider-popup-max-width',
isWidthLarger ? `${app.sliderSidebar.popupMaxWidth}%` : `${app.sliderSidebar.popupMaxWidth / 2.1}%`);
document.documentElement.style.setProperty('--slider-popup-max-height',
isWidthLarger ? '100%' : `${app.sliderSidebar.popupMaxWidth}%`);
// After the image has loaded, we calculate the actual popup height
const popupHeight = popup.offsetHeight;
const popupWidth = popup.offsetWidth;
// Check if the popup fits below the icon, if not, place it above
if (top + popupHeight > viewportHeight) {
// Calculate the new top position (above the icon)
top = rect.top + window.scrollY - popupHeight;
// Check if the popup now goes above the viewport (cut off at the top)
if (top < 0) {
top = 0; // Reposition so it stays fully in the viewport
}
}
// Ensure the popup doesn't overflow horizontally
if (left + popupWidth > viewportWidth) {
left = Math.max(0, viewportWidth - popupWidth);
}
// Apply the adjusted position after recalculating
popup.style.top = `${top}px`;
popup.style.left = `${left}px`;
// If the image is vertical, set the width to 5% less than the popupMaxWidth
if (!isWidthLarger) {
//this.style.width = `${app.sliderSidebar.popupMaxWidth - 5}%`;
}
}
});
// Append the image to the content container
contentContainer.appendChild(image);
};
// Show the first image
updateImage(images[currentIndex]);
} else {
const noImagesText = $el("p", { textContent: "No images available." });
contentContainer.appendChild(noImagesText);
}
})
.catch((error) => {
// Remove the loading text and show an error message
loadingText.textContent = "Failed to load images.";
console.error("Error fetching model images:", error);
});
// Update the current popup reference
this.currentPopup = popup;
// Close the popup when clicking outside
const closePopup = (event) => {
if (!popup.contains(event.target) && event.target !== iconElement) {
popup.remove();
document.removeEventListener("click", closePopup);
this.currentPopup = null;
}
};
setTimeout(() => {
document.addEventListener("click", closePopup);
}, 0);
}
async fetchModelImages(modelId) {
if (this.imageCache.has(modelId)) {
return this.imageCache.get(modelId); // Return the cached array of images
}
try {
const response = await fetch(`https://civitai.com/api/v1/models/${modelId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
// Extract all image URLs from modelVersions
const images = data.modelVersions[0]?.images.map(image => {
const originalUrl = image.url;
return originalUrl.replace(/\/width=\d+\//, '/'); // Correct the URL
}) || [];
if (images.length > 0) {
this.imageCache.set(modelId, images); // Cache the array of images
return images;
} else {
throw new Error("No images found for this model.");
}
} catch (error) {
console.error("Error fetching model images:", error);
throw error;
}
}
confirmDeleteSlider(slider) {
return new Promise((resolve) => {
const buttons = [
$el('button', {
type: 'button',
textContent: 'Yes',
onclick: () => {
dialog.close();
resolve(true);
}
}),
$el('button', {
type: 'button',
textContent: 'No',
onclick: () => {
dialog.close();
resolve(false);
}
})
];
const dialog = new ComfyDialog('div', buttons);
dialog.show(`Are you sure you want to delete the slider "${slider.name}"?`);
});
}
async deleteSlider(slider) {
try {
// Read the JSON data file
const data = await this.loadData();
// Remove the slider from customSliders
if (data.customSliders && Array.isArray(data.customSliders)) {
data.customSliders = data.customSliders.filter(
(s) => s.loraName !== slider.loraName
);
// Save the updated data back to the JSON file
await this.saveData(data);
this.debugLog("Custom sliders after deletion:", data.customSliders);
}
// Reload the custom sliders
await this.loadCustomSliders();
// Update the UI
this.update();
} catch (error) {
console.error('Error deleting custom slider:', error);
}
}
createSlider(hook) {
const container = $el("div.slider-container");
container.dataset.loraName = hook.loraName; // this is vital to all slider listeners so don't remove it
// First row: checkbox, name with info icon, author pill, and favorite icon
const firstRow = $el("div.slider-first-row");
firstRow.style.display = "flex";
firstRow.style.alignItems = "center";
firstRow.style.gap = "8px"; // Adjust spacing as needed
const toggleCheckbox = $el("input", {
type: "checkbox",
onchange: () => this.handleToggle(hook, toggleCheckbox.checked)
});
// Label container with info icon
const labelContainer = $el("label", { className: "label-container" });
// Introduce a <span> for the label text
const labelText = $el("span", {
textContent: this.getSliderLabel(hook),
className: "label-text editable-label", // For easy selection in updateSliderState
ondblclick: (e) => this.makeEditable(e.target, hook)
});
// Create the info icon
const infoIcon = $el("i", {
className: "pi pi-info-circle",
style: {
cursor: "pointer",
fontSize: "0.9em"
},
onclick: (e) => {
e.stopPropagation();
this.showModelInfoPopup(hook, infoIcon);
}
});
// Append labelText and infoIcon to labelContainer
labelContainer.appendChild(labelText);
labelContainer.appendChild(infoIcon);
if (hook.type === "Custom") {
const deleteIcon = $el("i", {
className: "pi pi-trash slider-delete-icon",
style: {
cursor: "pointer",
marginLeft: "10px",
color: "#dc3545" // Red color for delete
},
title: "Delete Slider",
onclick: (e) => {
e.stopPropagation(); // Prevent triggering other click events
this.confirmDeleteSlider(hook).then((confirmed) => {
if (confirmed) {
this.deleteSlider(hook);
}
});
}
});
labelContainer.appendChild(deleteIcon);
}
const authorPill = this.createAuthorPill(hook.author);
// Add favorite icon
const favoriteIcon = $el("i", {
className: this.favorites.has(hook.name) ? "pi pi-heart-fill" : "pi pi-heart",
style: {
cursor: "pointer",
marginLeft: "5px"
},
onclick: (e) => {
e.stopPropagation();
this.toggleFavorite(hook);
}
});
// Append elements to firstRow in the correct order
firstRow.appendChild(toggleCheckbox);
firstRow.appendChild(labelContainer);
firstRow.appendChild(authorPill);
firstRow.appendChild(favoriteIcon);
// Second row: slider, value input, and chkpt pill
const secondRow = $el("div.slider-second-row");
secondRow.style.display = "flex";
secondRow.style.alignItems = "center";
secondRow.style.gap = "1px"; // Adjust spacing as needed
const sliderConfig = this.getSliderConfig(hook);
const slider = $el("input", {
type: "range",
min: sliderConfig.min,
max: sliderConfig.max,
step: this.sliderStep,
value: sliderConfig.defaultValue,
oninput: (e) => this.updateSliderAndInput(hook, slider, valueInput, e.target.value)
});
const valueInput = $el("input.slider-value", {
type: "number",
value: sliderConfig.defaultValue,
step: this.sliderStep,
onchange: (e) => this.updateSliderAndInput(hook, slider, valueInput, e.target.value),
style: {
width: "60px"
}
});
const chkptPills = this.createChkptPills(hook.chkpt);
secondRow.appendChild(slider);
secondRow.appendChild(valueInput);
chkptPills.forEach(pill => secondRow.appendChild(pill));
// Append both rows to the container
container.appendChild(firstRow);
container.appendChild(secondRow);
const normalizedHookName = hook.loraName.replace(/\.(safetensors|ckpt|pt)$/, '').toLowerCase();
const isAvailable = this.availableLoras.map(name => name.toLowerCase()).includes(normalizedHookName);
// Set initial state
const node = this.findLoraNode(hook.loraName);
if (isAvailable) {
if (node) {
// LoRA is available and node exists in the graph
toggleCheckbox.checked = !node.mode;
slider.disabled = node.mode;
valueInput.disabled = node.mode;
} else {
// LoRA is available but node does not exist
// Set up drag-and-drop
this.enableDragAndDrop(container, hook);
container.classList.add('disabled');
// Enable UI elements
slider.disabled = false;
valueInput.disabled = true;
toggleCheckbox.checked = true;
}
} else {
container.classList.add('missing-lora', 'disabled');
// Disable drag and drop
container.draggable = false;
container.ondragstart = null;
// Disable UI elements
slider.disabled = true;
valueInput.disabled = true;
toggleCheckbox.disabled = true;
// Create Download button
const downloadButton = document.createElement('button');
downloadButton.innerHTML = 'Download <i class="pi pi-download" style="margin-left: 5px;"></i>';
downloadButton.classList.add('download-button');
// Append the download button inside the container
container.appendChild(downloadButton);
// Add click event listener
downloadButton.addEventListener('click', () => {
this.downloadLoraModel(hook.modelId, container);
});
}
return container;
}
updateSliderAndInput(hook, slider, input, value) {
// Ensure the value is within the slider's range
value = Math.max(slider.min, Math.min(slider.max, value));
// Update both slider and input
slider.value = value;
input.value = value;
// Call the original handleSliderChange method
this.handleSliderChange(hook, value);
}
handleSliderChange(hook, value) {
const node = this.findLoraNode(hook.loraName);
if (node) {
const strengthWidget = node.widgets.find(w => w.name === "strength_model");
if (strengthWidget) {
let adjustedValue = parseFloat(value);
if (this.fixReverseSliders && hook.reversed) {
adjustedValue = -adjustedValue;
}
strengthWidget.value = adjustedValue;
if (node.onWidgetChanged) {
node.onWidgetChanged("strength_model", adjustedValue);
}
app.graph.setDirtyCanvas(true, true);
}
}
}
handleToggle(hook, checked) {
const node = this.findLoraNode(hook.loraName);
if (node) {
node.mode = checked ? 0 : 4; // 0 for active, 4 for bypassed
this.debugLog(`Toggle changed for node ${node.id}: mode = ${node.mode}`);
app.graph.setDirtyCanvas(true, true);
} else {
this.debugLog(`No node found for Lora: ${hook.loraName}`);
}
}
updateSliderState(hook, node) {
const sliderContainer = this.accordion.querySelector(`[data-lora-name="${hook.loraName}"]`);
if (!sliderContainer) return;
const toggleCheckbox = sliderContainer.querySelector('input[type="checkbox"]');
const label = sliderContainer.querySelector('label');
const labelText = label.querySelector('.label-text'); // Target the <span> with class 'label-text'
const slider = sliderContainer.querySelector('input[type="range"]');
const valueSpan = sliderContainer.querySelector('.slider-value');
const valueInput = sliderContainer.querySelector('input[type="number"]');
// Update label text without affecting the info icon
if (labelText) {
labelText.textContent = this.getSliderLabel(hook);
}
if (node) {
// Node is present
sliderContainer.classList.remove('disabled');
slider.disabled = false; // Always enable the slider when node is present
valueInput.disabled = false; // Enable the numerical input
toggleCheckbox.checked = node.mode === 0; // Checked when mode is 0 (active)
toggleCheckbox.disabled = false; // Enable the checkbox
/* if (this.hideInactive && node.mode !== 0) {
// Hide inactive sliders, but show favorites if the setting is enabled
if (this.showFavorites && this.favorites.has(hook.name)) {
sliderContainer.style.display = '';
} else {
sliderContainer.style.display = 'none';
}
} else {
sliderContainer.style.display = '';
} */
// Update slider configuration
const sliderConfig = this.getSliderConfig(hook);
slider.min = sliderConfig.min;
slider.max = sliderConfig.max;
const strengthWidget = node.widgets.find(w => w.name === "strength_model");
if (strengthWidget && slider && valueInput) {
let displayValue = strengthWidget.value;
if (this.fixReverseSliders && hook.reversed) {
displayValue = -displayValue;
}
slider.value = displayValue;
valueInput.value = displayValue.toFixed(2);
if (valueSpan) valueSpan.textContent = displayValue; // Only update if found
}
this.disableDragAndDrop(sliderContainer);
} else {
// No node present
sliderContainer.classList.add('disabled');
toggleCheckbox.checked = false;
toggleCheckbox.disabled = true; // Disable the checkbox when no node
slider.disabled = true;
valueInput.disabled = true;
slider.value = hook.defaultValue;
if (valueSpan) {
valueSpan.textContent = hook.defaultValue;
}
/* if (this.hideInactive) {
// Hide inactive sliders, but show favorites if the setting is enabled
if (this.showFavorites && this.favorites.has(hook.name)) {
sliderContainer.style.display = '';
} else {
sliderContainer.style.display = 'none';
}
} else {
sliderContainer.style.display = '';
} */
this.enableDragAndDrop(sliderContainer, hook);
}
}
updateAllSliders() {
this.sliderHooks.forEach(hook => {
const node = this.findLoraNode(hook.loraName);
this.updateSliderState(hook, node);
// Reapply the current search after updating the accordion
this.handleSearch(this.currentSearchTerm || '');
// Then apply hide inactive
this.applyHideInactive();
});
}
findLoraNode(loraName) {
return app.graph._nodes.find(node =>
node.comfyClass === "LoraLoaderModelOnly" &&
node.widgets.some(w => w.name === "lora_name" && w.value === loraName)
);
}
enableDragAndDrop(container, hook) {
container.draggable = true;
container.ondragstart = (e) => {
// Avoid interfering with other drag events
e.stopPropagation();
console.log("Drag started for:", hook.name);
const sliderData = JSON.stringify({
comfySliderData: {
type: "comfy-slider",
data: hook
}
});
e.dataTransfer.setData("application/json", sliderData);
console.log("Setting dataTransfer with sliderJson:", sliderData);
};
}
disableDragAndDrop(container) {
container.draggable = false;
container.ondragstart = null;
}
setupCanvasDropHandling() {
const canvas = this.app.canvas.canvas;
const handleDrop = (e) => {
console.log("Drop event processed");
console.log("Data types:", e.dataTransfer.types);
// Get data with "application/json"
const sliderJson = e.dataTransfer.getData('application/json');
console.log("Received slider data:", sliderJson);
if (!sliderJson) {
console.log("Drop is not a slider, letting ComfyUI handle it");
return;
}
try {
const data = JSON.parse(sliderJson);
if (data.comfySliderData && data.comfySliderData.type === "comfy-slider") {
console.log("Processing comfy-slider drop");
const canvasRect = canvas.getBoundingClientRect();
const x = (e.clientX - canvasRect.left) / this.app.canvas.ds.scale - this.app.canvas.ds.offset[0];
const y = (e.clientY - canvasRect.top) / this.app.canvas.ds.scale - this.app.canvas.ds.offset[1];
this.createLoraNode(data.comfySliderData.data, x, y);
e.preventDefault();
e.stopPropagation();
} else {
console.log("Drop is not a slider, letting ComfyUI handle it");
}
} catch (error) {
console.error("Error processing slider data:", error);
}
};
if (!this._isCanvasDropHandlerAdded) {
canvas.addEventListener('dragover', (e) => {
if (e.dataTransfer.types.includes('application/json')) {
e.preventDefault();
}
});
canvas.addEventListener('drop', handleDrop);
this._isCanvasDropHandlerAdded = true;
}
}
createLoraNode(hook, x, y) {
console.log("Creating Lora node at:", x, y);
const node = LiteGraph.createNode("LoraLoaderModelOnly");
if (!node) {
console.error("Failed to create LoraLoaderModelOnly node");
return;
}
node.pos = [x, y];
// Set node title
node.title = `Lora - ${hook.name} Slider`;
// Set widget values
for (const widget of node.widgets) {
if (widget.name === "lora_name") {
widget.value = hook.loraName;
} else if (widget.name === "strength_model") {
widget.value = hook.defaultValue; // Use the new default value
}
}
// Add the node to the graph
app.graph.add(node);
// Find an existing nodes to connect to
if (this.smartWiring) {
const existingLoraNode = this.findExistingLoraNode();
if (existingLoraNode) {
this.wireNewLoraNode(node, existingLoraNode);
}
}
console.log("Node created and added to graph:", node);
// Ensure the canvas is updated
app.graph.setDirtyCanvas(true, true);
// Update the sidebar
this.updateAllSliders();
}
createAccordionSection(category, sliders) {
const section = $el("div.accordion-section");
section.dataset.category = category;
const header = $el("div.accordion-header", {
draggable: true,
dataset: { category: category },
onclick: (e) => {
if (e.target === header || e.target.tagName === 'I') {
this.toggleAccordion(section);
}
}
});
// Safely handle custom names: if no custom name, fall back to original category name
const displayName = this.categoryNames[category] || category;
// Create editable span for category name
const categoryNameSpan = $el("span.category-name.editable-label", {
textContent: displayName,
ondblclick: (e) => this.makeEditableCategory(e.target, category)
});
header.appendChild(categoryNameSpan);
// Create icon element and add to the header
const icon = $el("i.pi.pi-caret-down"); //default
icon.style.marginLeft = "auto"; // Ensures it is aligned to the right
header.appendChild(icon); // Append the icon to the header
const content = $el("div.accordion-content");
sliders.forEach(slider => {
if (!slider || !slider.loraName) {
console.warn('Invalid slider or missing loraName:', slider);
return; // Skip this slider
}
const normalizedLoraName = slider.loraName.replace(/\.(safetensors|ckpt|pt)$/, '').toLowerCase();
const isAvailable = this.availableLoras.includes(normalizedLoraName);
if (this.hideMissing && !isAvailable) {
return; // Skip adding this slider
}
const sliderElement = this.createSlider(slider);
content.appendChild(sliderElement);
});
section.appendChild(header);
section.appendChild(content);
// update carets as needed, placegholder until we save toggle state
// data.load.togglestate
return section;
}
toggleAccordion(section) {
const content = section.querySelector(".accordion-content");
const icon = section.querySelector("i");
if (content.style.display === "none") {
content.style.display = "block";
icon.classList.remove("pi-caret-left");
icon.classList.add("pi-caret-down");
} else {
content.style.display = "none";
icon.classList.remove("pi-caret-down");
icon.classList.add("pi-caret-left");
}
}
handleEnhance(hook, checked) {
console.log(`Enhance ${hook.name} changed to ${checked}`);
// Implement enhance logic here, possibly using hook.enhanceKeywords
}
onWorkflowLoaded() {
if (!this.accordion) return;
// Update all sliders to reflect the current state of nodes
this.sliderHooks.forEach(hook => {
const node = this.findLoraNode(hook.loraName);
if (node) {
const sliderContainer = this.accordion.querySelector(`[data-lora-name="${hook.loraName}"]`);
if (sliderContainer) {
const toggleCheckbox = sliderContainer.querySelector('input[type="checkbox"]');
const slider = sliderContainer.querySelector('input[type="range"]');
const strengthWidget = node.widgets.find(w => w.name === "strength_model");
if (toggleCheckbox) toggleCheckbox.checked = node.mode === 0; // Checked when mode is 0 (active)
if (slider) slider.disabled = node.mode === 4; // Disabled when mode is 4 (bypassed)
if (strengthWidget && slider) {
slider.value = strengthWidget.value;
const valueSpan = sliderContainer.querySelector('span');
if (valueSpan) valueSpan.textContent = strengthWidget.value;
}
}
}
});
}
filterSlidersByModel(sliders) {
if (this.modelFilter === "All") {
return sliders;
}
return sliders.filter(slider => {
const chkpts = (slider.chkpt || "").toLowerCase().split(',').map(c => c.trim());
switch (this.modelFilter) {
case "Pony":
return chkpts.some(c => c.includes("pony"));
case "SDXL":
return chkpts.some(c => c.includes("sdxl"));
case "Flux":
return chkpts.some(c => c.includes("flux"));
case "SD1.5":
return chkpts.some(c => c.includes("sd") && c.includes("1.5"));
case "Other":
return chkpts.every(c => !c.includes("pony") && !c.includes("sdxl") &&
!c.includes("flux") && !(c.includes("sd") && c.includes("1.5")));
default:
return true;
}
});
}
//LORA AUTO WIRING TEST, MADNESS
findExistingLoraNode() {
return app.graph._nodes.find(node =>
node.comfyClass === "LoraLoaderModelOnly" &&
node.outputs &&
node.outputs.length > 0 &&
node.outputs[0].links &&
node.outputs[0].links.length > 0
);
}
wireNewLoraNode(newNode, existingNode) {
if (!existingNode.outputs || !existingNode.outputs[0] || !existingNode.outputs[0].links) {
console.log("Existing node has no valid output connections");
return;
}
const existingOutputLinks = existingNode.outputs[0].links.map(linkId => app.graph.links[linkId]).filter(Boolean);
if (existingOutputLinks.length === 0) {
console.log("No valid output links found for existing node");
return;
}
// Connect the model input of the new node to the model output of the existing node
if (newNode.inputs && newNode.inputs[0] && existingNode.outputs && existingNode.outputs[0]) {
existingNode.connect(0, newNode, 0);
}
// Connect the output of the new node to all nodes the existing node was connected to
if (newNode.outputs && newNode.outputs[0]) {
existingOutputLinks.forEach(link => {
const targetNode = app.graph.getNodeById(link.target_id);
if (targetNode) {
newNode.connect(0, targetNode, link.target_slot);
}
});
}
// Remove all original connections from the existing node
existingOutputLinks.forEach(link => {
existingNode.disconnectOutput(0, link.target_id);
});
console.log(`New Lora node wired into the workflow, connected to ${existingOutputLinks.length} target(s)`);
}
///////
// NSFW STUFF BECAUSE I CARE
// Commented out dialog until it can go over settings, RIP
/*
showNSFWConfirmationDialog(newVal) {
const dialog = new ComfyDialog();
dialog.show($el("div", {}, [
$el("h3", {}, "Enable NSFW Content"),
$el("p", {}, "Are you sure you want to enable NSFW content? This may include adult or sensitive material."),
$el("p", {}, "Please ensure you are allowed to view this content and are in an appropriate setting."),
$el("div", {}, [
$el("button", {
type: "button",
textContent: "Cancel",
onclick: () => {
app.ui.settings.setSettingValue("Slider Sidebar.General.nsfw", false);
dialog.close();
}
}),
$el("button", {
type: "button",
textContent: "Confirm",
onclick: () => {
this.updateNSFWSetting(newVal);
dialog.close();
}
})
])
]));
}
*/
///CUSTOM SLIDERS OH MY!!!
async fetchLoraDetails() {
try {
const response = await api.fetchApi('/loras/list');
if (response.ok) {
const data = await response.json();
// You can use the entire object as needed
const loraDetails = data.map(item => ({
name: item.filename.replace(/\.(safetensors|ckpt|pt)$/, '').toLowerCase(),
path: item.path
}));
return loraDetails;
} else {
console.error('Failed to fetch LoRAs:', response.statusText);
}
} catch (error) {
console.error('Error fetching LoRAs:', error);
}
return [];
}
async showAddSliderDialog() {
try {
// Step 1: Fetch available LoRA files and their details
const loraFiles = await this.fetchAvailableLoras(); // Array of strings: filenames without extensions, all lowercase
this.loraDetails = await this.fetchLoraDetails(); // Assuming this remains unchanged
// Step 2: Extract existing LoRA filenames from sliderHooks (both custom and pre-packaged)
// Strip extensions from loraName and convert to lowercase to match loraFiles
const existingLoraNames = this.sliderHooks
.map(slider => {
if (typeof slider.loraName === 'string') {
return slider.loraName.replace(/\.(safetensors|pt)$/i, '').toLowerCase();
}
return null; // Handle unexpected types
})
.filter(name => name !== null); // Remove any null entries
// Step 3: Apply Filters to loraFiles
const filteredLoraFiles = loraFiles.filter(file => {
// No need to check extensions as fetchAvailableLoras already filters them
// Check if the file is not already present in sliderHooks
return !existingLoraNames.includes(file);
});
// Step 4: Initialize selection variables
let selectedFile = null;
let filteredFiles = [...filteredLoraFiles]; // Start with the filtered list
// Step 5: Create Search Input Element
const searchInput = $el("input", {
type: "text",
placeholder: "Search LoRAs...",
oninput: (e) => {
const searchTerm = e.target.value.toLowerCase();
filteredFiles = filteredLoraFiles.filter(file =>
file.toLowerCase().includes(searchTerm)
);
updateLoraList();
}
});
// Step 6: Create LoRA List Element
const loraList = $el("ul.lora-file-list");
// Step 7: Update LoRA List Display
const updateLoraList = () => {
loraList.innerHTML = "";
filteredFiles.forEach(file => {
const li = $el("li", {
textContent: file, // 'file' is a string
onclick: (e) => {
// Remove 'selected' class from all list items
loraList.querySelectorAll('li').forEach(li => li.classList.remove('selected'));
// Add 'selected' class to the clicked item
e.currentTarget.classList.add('selected');
// Set the selected file
selectedFile = file;
}
});
loraList.appendChild(li);
});
};
// Step 8: Initial Population of the List
updateLoraList();
// Step 9: Create Dialog Content
const content = $el("div", [
$el("h3", { textContent: "Select a LoRA file" }),
searchInput,
loraList
]);
// Step 10: Initialize Dialog
const dialog = new ComfyAsyncDialog([
{ value: "add", text: "Add Slider" },
{ value: "cancel", text: "Cancel" }
]);
// Step 11: Show Modal and Handle Result
try {
const result = await dialog.showModal(content);
if (result === "add" && selectedFile) {
await this.handleLoraFileSelection(selectedFile);
}
} catch (error) {
console.error("Error in file selection:", error);
} finally {
dialog.element.remove();
}
} catch (error) {
console.error("Error in showAddSliderDialog:", error);
}
}
async saveCustomSlider(customSlider) {
const data = await this.loadData();
if (!data.customSliders) {
data.customSliders = [];
}
data.customSliders.push(customSlider);
await this.saveData(data);
}
async loadCustomSliders() {
const data = await this.loadData();
if (data.customSliders && Array.isArray(data.customSliders)) {
this.sliderHooks = this.sliderHooks.filter(hook => hook.type !== "Custom");
this.sliderHooks = [...this.sliderHooks, ...data.customSliders];
}
if (data.sliderNames) {
this.sliderHooks.forEach(hook => {
if (data.sliderNames[hook.loraName]) {
hook.name = data.sliderNames[hook.loraName];
}
});
}
}
async handleLoraFileSelection(selectedName) {
try {
const selectedLora = this.loraDetails.find(lora => lora.name === selectedName);
if (!selectedLora) {
console.error("Selected LoRA not found:", selectedName);
return;
}
const filepath = selectedLora.path;
const filename = selectedLora.path.split(/[/\\]/).pop();
const hashResponse = await api.fetchApi('/hash_lora', {
method: 'POST',
body: JSON.stringify({ filepath }),
});
if (!hashResponse.ok) {
console.error("Error hashing LoRA file. Status:", hashResponse.status);
return;
}
const { hash } = await hashResponse.json();
const civitaiResponse = await fetch(`https://civitai.com/api/v1/model-versions/by-hash/${hash}`);
if (!civitaiResponse.ok) {
console.error("Error fetching model from Civitai. Status:", civitaiResponse.status);
return;
}
const modelData = await civitaiResponse.json();
const modelId = modelData.modelId;
if (!modelId) {
console.error("modelId not found in modelData:", modelData);
return;
}
// do ANOTHER call because Civit is super dumb and doesn't put the author in the hash API
const creatorResponse = await fetch(`https://civitai.com/api/v1/models/${modelId}`);
if (!creatorResponse.ok) {
console.error("Error fetching creator information from Civitai. Status:", creatorResponse.status);
return;
}
const creatorData = await creatorResponse.json();
const authorUsername = creatorData.creator?.username || "Unknown";
// Extract trained words from the latest model version
let enhanceKeywords = "";
if (creatorData.modelVersions && creatorData.modelVersions.length > 0) {
const latestVersion = creatorData.modelVersions[0];
if (latestVersion.trainedWords && latestVersion.trainedWords.length > 0) {
enhanceKeywords = latestVersion.trainedWords.join(", ");
}
}
const customSlider = {
name: creatorData.name || selectedName,
loraName: filename,
type: "Custom",
subtype: null,
minValue: -5,
maxValue: 5,
defaultValue: 0,
reversed: false,
enhanceKeywords: enhanceKeywords,
author: authorUsername,
modelId: modelId || "",
chkpt: modelData.baseModel || "Unknown"
};
this.sliderHooks.push(customSlider);
await this.saveCustomSlider(customSlider);
this.update();
console.log("Custom slider added successfully:", customSlider);
} catch (error) {
console.error("Error in handleLoraFileSelection:", error);
}
}
///////
/// Edit Name Stuff
makeEditable(element, hook) {
const currentText = element.textContent;
const input = $el("input", {
type: "text",
value: currentText,
onblur: (e) => this.saveEdit(e.target, element, hook),
onkeydown: (e) => {
if (e.key === "Enter") {
e.target.blur();
}
}
});
element.textContent = "";
element.appendChild(input);
input.focus();
}
async saveEdit(input, element, hook) {
const newName = input.value.trim();
if (newName && newName !== hook.name) {
hook.name = newName;
await this.saveCustomSliderName(hook);
}
element.textContent = this.getSliderLabel(hook);
}
async saveCustomSliderName(updatedHook) {
const data = await this.loadData();
if (!data.sliderNames) {
data.sliderNames = {};
}
data.sliderNames[updatedHook.loraName] = updatedHook.name;
await this.saveData(data);
}
makeEditableCategory(element, originalCategory) {
const currentText = element.textContent;
const input = $el("input", {
type: "text",
value: currentText,
onblur: (e) => this.saveEditCategory(e.target, element, originalCategory),
onkeydown: (e) => {
if (e.key === "Enter") {
e.target.blur();
}
}
});
element.textContent = "";
element.appendChild(input);
input.focus();
}
async saveEditCategory(input, element, originalCategory) {
const newCategoryName = input.value.trim();
if (newCategoryName && newCategoryName !== originalCategory) {
await this.saveCategoryName(originalCategory, newCategoryName);
this.updateCategoryName(originalCategory, newCategoryName);
}
element.textContent = newCategoryName || originalCategory;
}
async saveCategoryName(originalCategory, newCategoryName) {
const data = await this.loadData();
if (!data.categoryNames) {
data.categoryNames = {};
}
data.categoryNames[originalCategory] = newCategoryName;
await this.saveData(data);
// Update local categoryNames
this.categoryNames[originalCategory] = newCategoryName;
// Update UI
this.update();
}
updateCategoryName(originalCategory, newCategoryName) {
// Update the category name in the UI and internal data structures
const section = this.accordion.querySelector(`[data-category="${originalCategory}"]`);
if (section) {
section.dataset.category = newCategoryName;
section.querySelector('.category-name').textContent = newCategoryName;
}
if (this.categories[originalCategory]) {
this.categories[newCategoryName] = this.categories[originalCategory];
delete this.categories[originalCategory];
}
this.update();
}
applyCategoryNames(categoryNames) {
for (const [originalName, newName] of Object.entries(categoryNames)) {
if (this.categories[originalName]) {
this.categories[newName] = this.categories[originalName];
delete this.categories[originalName];
}
}
}
///////
///UPDATE SLIDER DATA STUFF
async checkForUpdates() {
const localVersion = await this.getLocalVersion();
const remoteVersion = await this.getRemoteVersion();
if (remoteVersion > localVersion) {
this.showUpdateButton();
}
}
async getLocalVersion() {
try {
console.log("Fetching local version...");
const response = await api.fetchApi('/slider_sidebar/version?type=local');
if (response.ok) {
const data = await response.json();
console.log("Local version data:", data);
return data.version;
}
} catch (error) {
console.error("Error fetching local version:", error);
}
return 0;
}
async getRemoteVersion() {
try {
console.log("Fetching remote version...");
const response = await api.fetchApi('/slider_sidebar/version?type=remote');
if (response.ok) {
const data = await response.json();
console.log("Remote version data:", data);
return data.version;
}
} catch (error) {
console.error("Error fetching remote version:", error);
}
return 0;
}
showUpdateButton() {
if (!this.updateButton) {
this.updateButton = $el("button.add-slider-button", {
textContent: "Update Slider Data",
onclick: () => this.updateSliderData()
});
this.updateButton.style.backgroundColor = "#4CAF50"; // Green color
this.element.insertBefore(this.updateButton, this.element.firstChild);
}
}
async updateSliderData() {
try {
const response = await api.fetchApi('/slider_sidebar/update', {
method: 'POST'
});
if (response.ok) {
await this.loadSliderData(); // Reload the slider data
this.update(); // Update the UI
this.updateButton.remove(); // Remove the update button
this.updateButton = null;
// Show a success message
app.extensionManager.toast.add({
severity: "success",
summary: "Update Successful",
detail: "Slider data has been updated.",
life: 3000
});
} else {
throw new Error("Failed to update slider data");
}
} catch (error) {
console.error("Error updating slider data:", error);
app.extensionManager.toast.add({
severity: "error",
summary: "Update Failed",
detail: "Failed to update slider data. Please try again.",
life: 3000
});
}
}
///////
updateNSFWSetting(newVal) {
this.nsfwEnabled = newVal;
this.update();
}
updateSliderStep(newVal) {
this.sliderStep = newVal;
this.update();
}
updatePopupMaxWidth(newVal) {
this.popupMaxWidth = newVal;
document.documentElement.style.setProperty('--slider-popup-max-width', `${this.popupMaxWidth}%`);
}
updatePillVisibility(newVal) {
this.showPills = newVal;
document.documentElement.style.setProperty('--slider-pill-opacity', this.showPills ? '1' : '0');
}
updateSmartWiring(newVal) {
this.smartWiring = newVal;
}
updateModelFilter(newVal) {
this.modelFilter = newVal;
this.update();
}
updateSliderLimits(newVal) {
this.ignoreSliderLimits = newVal;
this.update();
}
updateShowFavorites(newVal) {
this.showFavorites = newVal;
this.update();
}
///////
debugLog(message) {
if (this.debug) {
console.log(`[SliderSidebar Debug] ${message}`);
}
}
update() {
if (!this.accordion) return;
this.accordion.innerHTML = "";
this.processSliderHooks();
const allCategories = { ...this.categories, ...this.customCategories };
// Retrieve the 'fixedOrder' setting value
const fixedOrder = app.ui.settings.getSettingValue("Slider Sidebar.Visual.fixedOrder", true);
let orderedCategories;
if (fixedOrder) {
// Ensure 'Favorites' and 'Custom' are at the top in that order
orderedCategories = [
"Favorites",
"Custom",
...this.categoryOrder.filter(cat => cat !== "Favorites" && cat !== "Custom")
];
} else {
// Use the user-defined categoryOrder as is
orderedCategories = [...this.categoryOrder];
}
// Include any additional categories not specified in categoryOrder
const additionalCategories = Object.keys(allCategories).filter(cat => !orderedCategories.includes(cat));
orderedCategories = [...orderedCategories, ...additionalCategories];
const uniqueCategories = Array.from(new Set(orderedCategories));
for (const category of uniqueCategories) {
let sliders = [];
if (category === "Favorites") {
if (this.favorites.size === 0) continue; // Skip if no favorites
sliders = this.sliderHooks.filter(hook => this.favorites.has(hook.name));
} else if (category === "Custom") {
sliders = this.sliderHooks.filter(hook => hook.type === "Custom");
if (sliders.length === 0) continue; // Skip if no custom sliders
} else {
sliders = allCategories[category];
if (!sliders || sliders.length === 0) continue; // Skip if no sliders in category
// Exclude favorites from other categories
sliders = sliders.filter(hook => !this.favorites.has(hook.name));
if (sliders.length === 0) continue; // Skip if all sliders are favorites
}
// Apply model filter
sliders = this.filterSlidersByModel(sliders);
if (sliders.length === 0) continue;
const section = this.createAccordionSection(category, sliders);
this.accordion.appendChild(section);
// Trigger fade-in animation
setTimeout(() => {
section.classList.add('show');
}, 0);
}
// Setup drag and drop
this.setupDragAndDropForCategory();
// Update slider states after creating them
this.updateAllSliders();
// Setup canvas drop handling
this.setupCanvasDropHandling();
// Apply current search after updating the accordion
this.handleSearch(this.searchInput.value);
}
}
app.registerExtension({
name: "comfy.slider.sidebar",
async setup() {
const sliderSidebar = new SliderSidebar(app);
app.sliderSidebar = sliderSidebar;
app.ui.settings.addSetting({
id: "Slider Sidebar.Sliders.unreversed",
name: "'Fix' Reverse Sliders",
type: "boolean",
defaultValue: false,
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateFixReverseSlidersSettings(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Visual.hideMissing",
name: "Hide Sliders Missing Model Files",
type: "boolean",
defaultValue: false,
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateHideMissingSetting(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.General.apiKey",
name: "Civitai API Key (for Downloading)",
type: "text",
defaultValue: "",
placeholder: "Enter your Civitai API Key",
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateApiKey(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.General.nsfw",
name: "NSFW Sliders",
type: "boolean",
defaultValue: false,
onChange: (newVal, oldVal) => {
app.sliderSidebar.updateNSFWSetting(newVal);
/* if (newVal && !this.nsfwEnabled) {
app.sliderSidebar.showNSFWConfirmationDialog(newVal);
} else {
app.sliderSidebar.updateNSFWSetting(newVal);
} */
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Visual.fixedOrder",
name: "Keep 'Favorites' and 'Custom' At Top",
type: "boolean",
defaultValue: true, // Default to fixed order
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.update();
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.General.smartWires",
name: "Smart Node Wiring",
type: "boolean",
defaultValue: true, // Default to fixed order
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateSmartWiring(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Sliders.sliderLimits",
name: "Ingore Set Slider Limits",
type: "boolean",
defaultValue: false, // Default to fixed order
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateSliderLimits(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Visual.showModels",
name: "Base Models to Show",
type: 'combo',
options: ['All', 'Pony', 'Flux', 'SDXL', 'SD1.5', 'Other'],
defaultValue: 'All',
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateModelFilter(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Visual.showFavorites",
name: "Always Show Favorites",
type: 'boolean',
defaultValue: true,
onChange: (newVal, oldVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateShowFavorites(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Visual.popupMaxWidth",
name: "Popup Max Size (%)",
type: "slider",
defaultValue: 70,
attrs: { min: 1, max: 100, step: 5, },
onChange: (newVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updatePopupMaxWidth(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Sliders.sliderStep",
name: "Slider Increment",
type: "slider",
defaultValue: 0.25,
attrs: { min: 0.01, max: 1, step: 0.01, },
onChange: (newVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updateSliderStep(newVal);
}
}
});
app.ui.settings.addSetting({
id: "Slider Sidebar.Visual.showPills",
name: "Show Author and Checkpoint Tags",
type: "boolean",
defaultValue: true,
onChange: (newVal) => {
if (app.sliderSidebar) {
app.sliderSidebar.updatePillVisibility(newVal);
}
}
});
app.extensionManager.registerSidebarTab({
id: "slider.sidebar",
title: "Slider Sidebar",
icon: "pi pi-sliders-h",
tooltip: "Slider Sidebar",
type: "custom",
render: (el) => {
el.appendChild(sliderSidebar.element);
sliderSidebar.update();
sliderSidebar.setupNodeListeners();
}
});
},
});