Merge pull request #83 from ilian6806/forge

Forge support
develop
Ilian Iliev 2025-11-23 19:40:46 +02:00 committed by GitHub
commit 13f224c02f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 855 additions and 101 deletions

View File

@ -6,6 +6,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This is a Stable Diffusion WebUI extension that preserves UI parameters (inputs, sliders, checkboxes, etc.) after page reload. It uses localStorage for persistence and supports multiple SD extensions (ControlNet, ADetailer, Dynamic Prompting, Multidiffusion/Tiled VAE).
**Compatible with:**
- AUTOMATIC1111 Stable Diffusion WebUI
- Stable Diffusion WebUI Forge
- Gradio 3.x and 4.x
## Development
**No build system** - This is a pure extension with vanilla JavaScript and Python. Files are loaded directly by the parent Stable Diffusion WebUI.
@ -33,6 +38,15 @@ state.ext.*.js → Extension-specific handlers (ControlNet, ADetailer, et
- All code lives under `window.state` namespace (`state.core`, `state.store`, `state.utils`, `state.extensions`)
- Extension plugins register via `state.extensions[name] = { init: function() {} }`
**Gradio Compatibility Utilities** (`state.utils`):
- `state.utils.gradio.detectVersion()` - Detects Gradio 3.x vs 4.x
- `state.utils.getButtonClass()` - Returns appropriate button CSS classes
- `state.utils.findDropdowns(container)` - Finds dropdowns with fallback selectors
- `state.utils.findAccordions(container)` - Finds accordions with fallback selectors
- `state.utils.getDropdownValue(select)` - Gets dropdown value (works with both Gradio versions)
- `state.utils.getMultiSelectValues(select)` - Gets multi-select values
- `state.utils.isAccordionOpen(accordion)` - Checks accordion state with multiple methods
### Backend (Python)
```

View File

@ -14,6 +14,7 @@ state.core = (function () {
'hires_resize_x': 'hr_resize_x',
'hires_resize_y': 'hr_resize_y',
'hires_denoising_strength': 'denoising_strength',
'hires_cfg_scale': 'hr_cfg',
'refiner_switch': 'switch_at',
'upscaler_2_visibility': 'extras_upscaler_2_visibility',
'upscaler_scale_by_resize': 'extras_upscaling_resize',
@ -225,13 +226,47 @@ state.core = (function () {
}
}
// Get tab buttons with multiple fallback selectors for compatibility
function getTabButtons() {
var root = gradioApp();
// Try multiple selectors for compatibility with different Gradio versions
var tabs = root.querySelectorAll('#tabs > div:first-child button');
if (!tabs.length) {
tabs = root.querySelectorAll('#tabs .tab-nav button');
}
if (!tabs.length) {
tabs = root.querySelectorAll('#tabs > .tabs > .tab-nav button');
}
if (!tabs.length) {
// Gradio 4.x may have different structure
tabs = root.querySelectorAll('#tabs button[role="tab"]');
}
if (!tabs.length) {
tabs = root.querySelectorAll('#tabs > div > button');
}
return tabs;
}
// Get selected tab button with fallback
function getSelectedTabButton() {
var root = gradioApp();
var selected = root.querySelector('#tabs .tab-nav button.selected');
if (!selected) {
selected = root.querySelector('#tabs button.selected');
}
if (!selected) {
selected = root.querySelector('#tabs button[aria-selected="true"]');
}
return selected;
}
function restoreTabs(config) {
if (! config.hasSetting('tabs')) {
return;
}
const tabs = gradioApp().querySelectorAll('#tabs > div:first-child button');
const tabs = getTabButtons();
const value = store.get('tab');
if (value) {
@ -244,20 +279,23 @@ state.core = (function () {
}
// Use this when onUiTabChange is fixed
// onUiTabChange(function () {
// store.set('tab', gradioApp().querySelector('#tabs .tab-nav button.selected').textContent);
// store.set('tab', getSelectedTabButton()?.textContent);
// });
bindTabClickEvents();
}
function bindTabClickEvents() {
Array.from(gradioApp().querySelectorAll('#tabs .tab-nav button')).forEach(tab => {
Array.from(getTabButtons()).forEach(tab => {
tab.removeEventListener('click', storeTab);
tab.addEventListener('click', storeTab);
});
}
function storeTab() {
store.set('tab', gradioApp().querySelector('#tabs .tab-nav button.selected').textContent);
var selected = getSelectedTabButton();
if (selected) {
store.set('tab', selected.textContent);
}
bindTabClickEvents(); // dirty hack here...
}
@ -288,6 +326,12 @@ state.core = (function () {
return;
}
// Convert to array for easier handling
elements = Array.from(elements);
// For sliders with both number input and range, prefer number input for storage
let primaryElement = elements.find(el => el.type === 'number') || elements[0];
let forEach = function (action) {
events.forEach(function(event) {
elements.forEach(function (element) {
@ -296,6 +340,7 @@ state.core = (function () {
});
};
// Attach event listeners to all elements
forEach(function (event) {
this.addEventListener(event, function () {
let value = this.value;
@ -306,17 +351,23 @@ state.core = (function () {
});
});
// Special handling for seed buttons
if (id.indexOf('seed') > -1) {
TABS.forEach(tab => {
const seedInput = gradioApp().querySelector(`#${tab}_seed input`);
['random_seed', 'reuse_seed'].forEach(id => {
const btn = gradioApp().querySelector(`#${tab}_${id}`);
const seedInput = gradioApp().querySelector(`#${tab}_seed input[type="number"]`);
if (!seedInput) return;
['random_seed', 'reuse_seed'].forEach(btnId => {
const btn = gradioApp().querySelector(`#${tab}_${btnId}`);
if (btn) {
btn.addEventListener('click', () => {
setTimeout(() => {
state.utils.triggerEvent(seedInput, 'change');
}, 100);
});
}
});
});
}
let value = store.get(id);
@ -324,10 +375,68 @@ state.core = (function () {
return;
}
forEach(function (event) {
state.utils.setValue(this, value, event);
// Delay restoration to ensure UI and updateInput are ready
// Use longer delay to ensure Gradio components are fully initialized
setTimeout(function() {
// Check if updateInput is available (it should be by now)
if (typeof updateInput !== 'function') {
state.logging.warn('updateInput not available yet, retrying...');
setTimeout(function() {
restoreInputValue(id, elements, value);
}, 500);
} else {
restoreInputValue(id, elements, value);
}
}, 1000);
}
function restoreInputValue(id, elements, value) {
state.logging.log(`Attempting to restore ${id} with value: ${value}`);
// Check element types
let numberInput = elements.find(el => el.type === 'number');
let rangeInput = elements.find(el => el.type === 'range');
let textareaInput = elements.find(el => el.tagName === 'TEXTAREA');
let textInput = elements.find(el => el.type === 'text');
state.logging.log(`Found elements for ${id}: number=${!!numberInput}, range=${!!rangeInput}, textarea=${!!textareaInput}, text=${!!textInput}`);
// For sliders (have both number and range), use special handler
if (numberInput && rangeInput) {
// Get the container element
let container = numberInput.closest('.gradio-slider, [class*="slider"]') ||
numberInput.parentElement.parentElement;
state.logging.log(`Updating slider container for ${id}`, container);
state.utils.updateGradioSlider(container, value);
// Verify the value was set
setTimeout(function() {
state.logging.log(`After restore - ${id} number value: ${numberInput.value}, range value: ${rangeInput.value}`);
}, 100);
} else if (numberInput) {
// Just number input
state.utils.setValue(numberInput, value, 'input');
state.logging.log(`Restored number ${id}: ${value}`);
} else if (textareaInput) {
// For textareas (prompts)
state.utils.setValue(textareaInput, value, 'input');
state.logging.log(`Restored textarea ${id}: ${value}`);
} else if (textInput) {
// For text inputs
state.utils.setValue(textInput, value, 'input');
state.logging.log(`Restored text ${id}: ${value}`);
} else if (rangeInput) {
// Fallback to range input
state.utils.setValue(rangeInput, value, 'input');
state.logging.log(`Restored range ${id}: ${value}`);
} else {
// For any other elements, update all
state.logging.log(`Restoring ${id} to all ${elements.length} elements`);
elements.forEach(function (element) {
state.utils.setValue(element, value, 'input');
});
}
}
function handleSavedSelects(id, duplicateIds) {
if (duplicateIds) {
@ -353,15 +462,29 @@ state.core = (function () {
let selector = duplicateIds ? `[id="${id}"]` : `#${id}`;
elements = gradioApp().querySelectorAll(`.input-accordion${selector}>.label-wrap`);
// Try multiple selector patterns for compatibility
var elements = gradioApp().querySelectorAll(`.input-accordion${selector}>.label-wrap`);
if (!elements.length) {
elements = gradioApp().querySelectorAll(`.input-accordion${selector} .label-wrap`);
}
if (!elements.length) {
elements = gradioApp().querySelectorAll(`${selector} > .label-wrap`);
}
if (!elements.length) {
elements = gradioApp().querySelectorAll(`${selector}.input-accordion > .label-wrap`);
}
elements.forEach(function (element) {
if (store.get(id) === 'true') {
state.utils.clickToggleMenu(element);
}
element.addEventListener('click', function () {
let classList = Array.from(this.parentNode.classList);
store.set(id, classList.indexOf('input-accordion-open') > -1);
var parent = this.parentNode;
// Check for open state using multiple methods for compatibility
var isOpen = parent.classList.contains('input-accordion-open') ||
this.classList.contains('open') ||
state.utils.isAccordionOpen(parent);
store.set(id, isOpen);
});
});
}

View File

@ -9,7 +9,14 @@ state.extensions['adetailer'] = (function () {
let cnTabs = [];
function bindTabEvents() {
const tabs = container.querySelectorAll('.tabs > div > button');
// Try multiple selectors for compatibility
let tabs = container.querySelectorAll('.tabs > div > button');
if (!tabs.length) {
tabs = container.querySelectorAll('.tabs .tab-nav button');
}
if (!tabs.length) {
tabs = container.querySelectorAll('button[role="tab"]');
}
tabs.forEach(tab => { // dirty hack here
tab.removeEventListener('click', onTabClick);
tab.addEventListener('click', onTabClick);
@ -36,6 +43,8 @@ state.extensions['adetailer'] = (function () {
}
function handleCheckbox(checkbox, id) {
if (!checkbox) return;
let value = store.get(id);
if (value) {
state.utils.setValue(checkbox, value, 'change');
@ -81,7 +90,8 @@ state.extensions['adetailer'] = (function () {
}
function handleSelects(container, container_idx) {
let selects = container.querySelectorAll('.gradio-dropdown')
// Use compatibility helper to find dropdowns
let selects = state.utils.findDropdowns(container);
selects.forEach(function (select, idx) {
state.utils.handleSelect(select, `ad-tab-${container_idx}-select-${idx}`, store);
});
@ -107,28 +117,49 @@ state.extensions['adetailer'] = (function () {
}
function handleDropdown(dropdown, id) {
if (!dropdown) return;
let value = store.get(id);
if (value && value === 'true') {
state.utils.triggerEvent(dropdown, 'click');
}
dropdown.addEventListener('click', function () {
let span = this.querySelector('.transition, .icon');
store.set(id, span.style.transform !== 'rotate(90deg)');
// Use multiple methods to check open state for compatibility
let isOpen = this.classList.contains('open') ||
this.parentNode.classList.contains('open') ||
state.utils.isAccordionOpen(this.parentNode);
store.set(id, isOpen);
});
}
function handleDropdowns(container, container_idx) {
let dropdowns = container.querySelectorAll('.gradio-accordion .label-wrap');
dropdowns.forEach(function (dropdown, idx) {
handleDropdown(dropdown, `ad-tab-${container_idx}-dropdown-${idx}`);
// Use compatibility helper to find accordions
let accordions = state.utils.findAccordions(container);
accordions.forEach(function (accordion, idx) {
let labelWrap = accordion.querySelector('.label-wrap');
if (labelWrap) {
handleDropdown(labelWrap, `ad-tab-${container_idx}-dropdown-${idx}`);
}
});
}
function load() {
setTimeout(function () {
handleDropdown(container.querySelector('#script_txt2img_adetailer_ad_main_accordion > .label-wrap'), 'ad-dropdown-main');
handleCheckbox(container.querySelector('#script_txt2img_adetailer_ad_enable > label > input'), 'ad-checkbox-enable');
// Try multiple selectors for the main accordion
let mainAccordion = container.querySelector('#script_txt2img_adetailer_ad_main_accordion > .label-wrap');
if (!mainAccordion) {
mainAccordion = container.querySelector('.label-wrap');
}
handleDropdown(mainAccordion, 'ad-dropdown-main');
// Try multiple selectors for the enable checkbox
let enableCheckbox = container.querySelector('#script_txt2img_adetailer_ad_enable > label > input');
if (!enableCheckbox) {
enableCheckbox = container.querySelector('input[type="checkbox"]');
}
handleCheckbox(enableCheckbox, 'ad-checkbox-enable');
cnTabs.forEach(({ container, container_idx }) => {
handleTabs(container, container_idx);
handleTextboxes(container, container_idx);
@ -143,14 +174,30 @@ state.extensions['adetailer'] = (function () {
function init() {
// Try multiple selectors for ADetailer container (Forge vs A1111 compatibility)
container = gradioApp().getElementById('script_txt2img_adetailer_ad_main_accordion');
store = new state.Store('ext-adetailerr');
if (!container) {
container = gradioApp().querySelector('[id*="adetailer"]');
}
if (!container) {
container = gradioApp().querySelector('[id*="ADetailer"]');
}
store = new state.Store('ext-adetailer');
if (!container) {
state.logging.log('ADetailer extension not found');
return;
}
// Try multiple selectors for tabs
let tabs = container.querySelectorAll('.tabitem');
if (!tabs.length) {
tabs = container.querySelectorAll('[id*="tabitem"]');
}
if (!tabs.length) {
tabs = container.querySelectorAll('.tabs > div[role="tabpanel"]');
}
if (tabs.length) {
cnTabs = [];
@ -162,7 +209,8 @@ state.extensions['adetailer'] = (function () {
});
} else {
cnTabs = [{
container: container
container: container,
container_idx: 0
}];
}

View File

@ -11,7 +11,17 @@ function ControlNetTabContext(tabName, container) {
this.tabElements = [];
this.cnTabs = [];
// Try multiple selectors for compatibility with different Gradio/Forge versions
let tabs = this.container.querySelectorAll(':scope > div > div > .tabs > .tabitem');
if (!tabs.length) {
tabs = this.container.querySelectorAll('.tabitem');
}
if (!tabs.length) {
tabs = this.container.querySelectorAll('[id*="tabitem"]');
}
if (!tabs.length) {
tabs = this.container.querySelectorAll('.tabs > div[role="tabpanel"]');
}
if (tabs.length) {
tabs.forEach((tabContainer, i) => {
@ -21,10 +31,10 @@ function ControlNetTabContext(tabName, container) {
});
});
} else {
this.cnTabs.push[{
this.cnTabs.push({
container: container,
store: new state.Store(`ext-control-net-${this.tabName}-${i}`)
}];
store: new state.Store(`ext-control-net-${this.tabName}-0`)
});
}
}
@ -38,7 +48,11 @@ state.extensions['control-net'] = (function () {
contexts.forEach(context => {
const elements = context.container.querySelectorAll(`:scope > .label-wrap`)
// Try multiple selectors for compatibility
let elements = context.container.querySelectorAll(`:scope > .label-wrap`);
if (!elements.length) {
elements = context.container.querySelectorAll('.label-wrap');
}
elements.forEach(element => {
if (context.store.get(id) === 'true') {
@ -46,8 +60,11 @@ state.extensions['control-net'] = (function () {
load();
}
element.addEventListener('click', function () {
let classList = Array.from(this.classList);
context.store.set(id, classList.indexOf('open') > -1);
// Check for open state using multiple methods for compatibility
let isOpen = this.classList.contains('open') ||
this.parentNode.classList.contains('open') ||
state.utils.isAccordionOpen(this.parentNode);
context.store.set(id, isOpen);
load();
});
});
@ -56,7 +73,18 @@ state.extensions['control-net'] = (function () {
function bindTabEvents() {
contexts.forEach(context => {
const tabs = context.container.querySelectorAll(':scope > div > div > .tabs > div > button');
// Try multiple selectors for compatibility
let tabs = context.container.querySelectorAll(':scope > div > div > .tabs > div > button');
if (!tabs.length) {
tabs = context.container.querySelectorAll('.tabs .tab-nav button');
}
if (!tabs.length) {
tabs = context.container.querySelectorAll('.tabs > div > button');
}
if (!tabs.length) {
tabs = context.container.querySelectorAll('button[role="tab"]');
}
function onTabClick() {
context.store.set('tab', this.textContent);
bindTabEvents();
@ -92,12 +120,84 @@ state.extensions['control-net'] = (function () {
});
}
// Helper to find checkbox label text
function getCheckboxLabel(checkbox) {
// Try nextElementSibling (common pattern)
if (checkbox.nextElementSibling && checkbox.nextElementSibling.textContent) {
return checkbox.nextElementSibling.textContent;
}
// Try parent label
let parentLabel = checkbox.closest('label');
if (parentLabel) {
// Get text excluding the checkbox itself
let clone = parentLabel.cloneNode(true);
let input = clone.querySelector('input');
if (input) input.remove();
if (clone.textContent && clone.textContent.trim()) {
return clone.textContent.trim();
}
}
// Try aria-label or title
if (checkbox.getAttribute('aria-label')) return checkbox.getAttribute('aria-label');
if (checkbox.title) return checkbox.title;
return null;
}
// Helper to find select/dropdown label text
function getSelectLabel(select) {
// Try label inside the select container
let label = select.querySelector('label');
if (label) {
if (label.firstChild && label.firstChild.textContent) {
return label.firstChild.textContent;
}
if (label.textContent) return label.textContent;
}
// Try span with label class
let span = select.querySelector('span[data-testid="block-label"], span[class*="label"]');
if (span && span.textContent) return span.textContent;
// Try previous sibling
if (select.previousElementSibling) {
let prevLabel = select.previousElementSibling.querySelector('label, span');
if (prevLabel && prevLabel.textContent) return prevLabel.textContent;
}
return null;
}
// Helper to find textarea label text
function getTextareaLabel(textarea) {
// Try previousElementSibling
if (textarea.previousElementSibling && textarea.previousElementSibling.textContent) {
return textarea.previousElementSibling.textContent;
}
// Try parent container for label
let parent = textarea.closest('.gradio-textbox, [class*="textbox"]');
if (parent) {
let label = parent.querySelector('label, span[data-testid="block-label"]');
if (label && label.textContent) return label.textContent;
}
// Try aria-label or placeholder
if (textarea.getAttribute('aria-label')) return textarea.getAttribute('aria-label');
if (textarea.placeholder) return textarea.placeholder;
return null;
}
function handleCheckboxes() {
handleContext((container, store) => {
let checkboxes = container.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(function (checkbox) {
let label = checkbox.nextElementSibling;
let id = state.utils.txtToId(label.textContent);
checkboxes.forEach(function (checkbox, idx) {
let labelText = getCheckboxLabel(checkbox);
// Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `checkbox-${idx}`;
let value = store.get(id);
if (value) {
state.utils.setValue(checkbox, value, 'change');
@ -111,8 +211,12 @@ state.extensions['control-net'] = (function () {
function handleSelects() {
handleContext((container, store) => {
container.querySelectorAll('.gradio-dropdown').forEach(select => {
let id = state.utils.txtToId(select.querySelector('label').firstChild.textContent);
// Use compatibility helper to find dropdowns
let dropdowns = state.utils.findDropdowns(container);
dropdowns.forEach(function (select, idx) {
let labelText = getSelectLabel(select);
// Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `select-${idx}`;
let value = store.get(id);
state.utils.handleSelect(select, id, store);
if (id === 'preprocessor' && value && value.toLowerCase() !== 'none') {
@ -122,12 +226,64 @@ state.extensions['control-net'] = (function () {
});
}
// Helper to find slider label text with multiple fallback methods
function getSliderLabel(slider) {
// Try previousElementSibling first (old structure)
if (slider.previousElementSibling) {
let label = slider.previousElementSibling.querySelector('label span');
if (label && label.textContent) return label.textContent;
// Try just the label
label = slider.previousElementSibling.querySelector('label');
if (label && label.textContent) return label.textContent;
// Try span directly
label = slider.previousElementSibling.querySelector('span');
if (label && label.textContent) return label.textContent;
}
// Try parent container for label (Forge/Gradio 4.x structure)
let parent = slider.closest('.gradio-slider, .slider, [class*="slider"]');
if (parent) {
let label = parent.querySelector('label span, label, span[data-testid="block-label"]');
if (label && label.textContent) return label.textContent;
}
// Try looking for label in parent's previous sibling
if (slider.parentElement && slider.parentElement.previousElementSibling) {
let label = slider.parentElement.previousElementSibling.querySelector('span, label');
if (label && label.textContent) return label.textContent;
}
return null;
}
// Helper to find fieldset/radio label text
function getFieldsetLabel(fieldset) {
// Try firstChild.nextElementSibling (old structure)
if (fieldset.firstChild && fieldset.firstChild.nextElementSibling) {
let label = fieldset.firstChild.nextElementSibling;
if (label && label.textContent) return label.textContent;
}
// Try legend element
let legend = fieldset.querySelector('legend');
if (legend && legend.textContent) return legend.textContent;
// Try label or span
let label = fieldset.querySelector('label, span[class*="label"]');
if (label && label.textContent) return label.textContent;
return null;
}
function handleSliders() {
handleContext((container, store) => {
let sliders = container.querySelectorAll('input[type="range"]');
sliders.forEach(function (slider) {
let label = slider.previousElementSibling.querySelector('label span');
let id = state.utils.txtToId(label.textContent);
sliders.forEach(function (slider, idx) {
let labelText = getSliderLabel(slider);
// Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `slider-${idx}`;
let value = store.get(id);
if (value) {
state.utils.setValue(slider, value, 'change');
@ -142,10 +298,11 @@ state.extensions['control-net'] = (function () {
function handleRadioButtons() {
handleContext((container, store) => {
let fieldsets = container.querySelectorAll('fieldset');
fieldsets.forEach(function (fieldset) {
let label = fieldset.firstChild.nextElementSibling;
fieldsets.forEach(function (fieldset, idx) {
let radios = fieldset.querySelectorAll('input[type="radio"]');
let id = state.utils.txtToId(label.textContent);
let labelText = getFieldsetLabel(fieldset);
// Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `radio-${idx}`;
let value = store.get(id);
if (value) {
radios.forEach(function (radio) {
@ -164,9 +321,10 @@ state.extensions['control-net'] = (function () {
function handleTextareas() {
handleContext((container, store) => {
let textareas = container.querySelectorAll('textarea');
textareas.forEach(function (textarea) {
let label = textarea.previousElementSibling;
let id = state.utils.txtToId(label.textContent);
textareas.forEach(function (textarea, idx) {
let labelText = getTextareaLabel(textarea);
// Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `textarea-${idx}`;
let value = store.get(id);
if (value) {
state.utils.setValue(textarea, value, 'change');
@ -191,14 +349,32 @@ state.extensions['control-net'] = (function () {
function init() {
// Try multiple selectors for ControlNet container (Forge vs A1111 compatibility)
let elements = gradioApp().querySelectorAll('#controlnet');
if (!elements.length) {
elements = gradioApp().querySelectorAll('[id*="controlnet"]');
}
if (!elements.length) {
elements = gradioApp().querySelectorAll('#txt2img_controlnet, #img2img_controlnet');
}
// Forge built-in ControlNet uses different IDs
if (!elements.length) {
elements = gradioApp().querySelectorAll('[id*="forge_controlnet"], [id*="sd_forge_controlnet"]');
}
if (!elements.length) {
state.logging.log('ControlNet extension not found');
return;
}
// Handle both single container and separate txt2img/img2img containers
if (elements.length >= 2) {
contexts[0] = new ControlNetTabContext('txt2img', elements[0]);
contexts[1] = new ControlNetTabContext('img2img', elements[1]);
} else if (elements.length === 1) {
// Single container mode
contexts[0] = new ControlNetTabContext('main', elements[0]);
}
handleToggle();
load();

View File

@ -50,7 +50,8 @@ state.extensions['dynamic prompting'] = (function () {
}
function handleSelects() {
let selects = container.querySelectorAll('.gradio-dropdown')
// Use compatibility helper to find dropdowns
let selects = state.utils.findDropdowns(container);
selects.forEach(function (select, idx) {
state.utils.handleSelect(select, `dp-select-${idx}`, store);
});
@ -76,17 +77,24 @@ state.extensions['dynamic prompting'] = (function () {
}
function handleDropdowns() {
let dropdowns = container.querySelectorAll('.gradio-accordion .label-wrap');
dropdowns.forEach(function (dropdown, idx) {
// Use compatibility helper to find accordions
let accordions = state.utils.findAccordions(container);
accordions.forEach(function (accordion, idx) {
let labelWrap = accordion.querySelector('.label-wrap');
if (!labelWrap) return;
let id = `dp-dropdown-${idx}`;
let value = store.get(id);
if (value && value === 'true') {
state.utils.triggerEvent(dropdown, 'click');
state.utils.triggerEvent(labelWrap, 'click');
}
dropdown.addEventListener('click', function () {
let span = this.querySelector('.transition, .icon');
store.set(id, span.style.transform !== 'rotate(90deg)');
labelWrap.addEventListener('click', function () {
// Use multiple methods to check open state for compatibility
let isOpen = this.classList.contains('open') ||
this.parentNode.classList.contains('open') ||
state.utils.isAccordionOpen(this.parentNode);
store.set(id, isOpen);
});
});
}
@ -104,10 +112,22 @@ state.extensions['dynamic prompting'] = (function () {
function init() {
// Try multiple selectors for Dynamic Prompting container (Forge vs A1111 compatibility)
container = gradioApp().getElementById('sddp-dynamic-prompting');
if (!container) {
container = gradioApp().querySelector('[id*="dynamic-prompting"]');
}
if (!container) {
container = gradioApp().querySelector('[id*="dynamicprompts"]');
}
if (!container) {
container = gradioApp().querySelector('[id*="dynamic_prompts"]');
}
store = new state.Store('ext-dynamic-prompting');
if (!container) {
state.logging.log('Dynamic Prompting extension not found');
return;
}

View File

@ -50,7 +50,8 @@ state.extensions['multidiffusion'] = (function () {
}
function handleSelects(container, name) {
let selects = container.querySelectorAll('.gradio-dropdown')
// Use compatibility helper to find dropdowns
let selects = state.utils.findDropdowns(container);
selects.forEach(function (select, idx) {
state.utils.handleSelect(select, `md-${name}-select-${idx}`, store);
});
@ -76,17 +77,24 @@ state.extensions['multidiffusion'] = (function () {
}
function handleDropdowns(container, name) {
let dropdowns = container.querySelectorAll('.gradio-accordion .label-wrap');
dropdowns.forEach(function (dropdown, idx) {
// Use compatibility helper to find accordions
let accordions = state.utils.findAccordions(container);
accordions.forEach(function (accordion, idx) {
let labelWrap = accordion.querySelector('.label-wrap');
if (!labelWrap) return;
let id = `md-${name}-dropdown-${idx}`;
let value = store.get(id);
if (value && value === 'true') {
state.utils.triggerEvent(dropdown, 'click');
state.utils.triggerEvent(labelWrap, 'click');
}
dropdown.addEventListener('click', function () {
let span = this.querySelector('.transition, .icon');
store.set(id, span.style.transform !== 'rotate(90deg)');
labelWrap.addEventListener('click', function () {
// Use multiple methods to check open state for compatibility
let isOpen = this.classList.contains('open') ||
this.parentNode.classList.contains('open') ||
state.utils.isAccordionOpen(this.parentNode);
store.set(id, isOpen);
});
});
}
@ -106,20 +114,47 @@ state.extensions['multidiffusion'] = (function () {
function init() {
// Try to find Tiled Diffusion/VAE containers by text content
let spanTags = gradioApp().getElementsByTagName("span");
for (var i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent == 'Tiled Diffusion') {
containers.push({container: spanTags[i].parentElement.parentElement,name: 'diffusion'});
let text = spanTags[i].textContent.trim();
if (text === 'Tiled Diffusion' || text === 'Tiled Diffusion (Multidiffusion)') {
let parent = spanTags[i].parentElement;
// Navigate up to find the accordion container
while (parent && !parent.classList.contains('gradio-accordion') && !parent.classList.contains('accordion') && !parent.id) {
parent = parent.parentElement;
}
if (parent) {
containers.push({container: parent, name: 'diffusion'});
}
}
if (text === 'Tiled VAE') {
let parent = spanTags[i].parentElement;
while (parent && !parent.classList.contains('gradio-accordion') && !parent.classList.contains('accordion') && !parent.id) {
parent = parent.parentElement;
}
if (parent) {
containers.push({container: parent, name: 'vae'});
}
}
}
// Also try by ID for Forge compatibility
if (!containers.length) {
let tiledDiff = gradioApp().querySelector('[id*="tiled_diffusion"], [id*="multidiffusion"]');
if (tiledDiff) {
containers.push({container: tiledDiff, name: 'diffusion'});
}
let tiledVae = gradioApp().querySelector('[id*="tiled_vae"]');
if (tiledVae) {
containers.push({container: tiledVae, name: 'vae'});
}
if (spanTags[i].textContent == 'Tiled VAE') {
containers.push({container: spanTags[i].parentElement.parentElement,name: 'vae'});
break;
}
};
store = new state.Store('ext-multidiffusion');
if (!containers.length) {
state.logging.log('Multidiffusion/Tiled VAE extension not found');
return;
}

View File

@ -5,15 +5,45 @@ state.logging = {
name: 'state',
log: function (message) {
// Set to true to enable debug logging
DEBUG: false,
log: function (message, data) {
if (!this.DEBUG) return;
if (data !== undefined) {
console.log(`[${this.name}]: `, message, data);
} else {
console.log(`[${this.name}]: `, message);
}
},
error: function (message) {
error: function (message, data) {
// Errors are always shown
if (data !== undefined) {
console.error(`[${this.name}]: `, message, data);
} else {
console.error(`[${this.name}]: `, message);
}
},
warn: function (message) {
warn: function (message, data) {
if (!this.DEBUG) return;
if (data !== undefined) {
console.warn(`[${this.name}]: `, message, data);
} else {
console.warn(`[${this.name}]: `, message);
}
},
// Call this in browser console to enable debugging: state.logging.enable()
enable: function() {
this.DEBUG = true;
console.log(`[${this.name}]: Debug logging enabled`);
},
// Call this in browser console to disable debugging: state.logging.disable()
disable: function() {
this.DEBUG = false;
console.log(`[${this.name}]: Debug logging disabled`);
}
};

View File

@ -46,10 +46,107 @@ state.utils = {
element.checked = false;
}
break;
case 'number':
case 'range':
// For sliders and number inputs, use Forge pattern
element.value = value;
if (typeof updateInput === 'function') {
updateInput(element);
}
break;
case 'textarea':
// Textareas (prompts) - use Forge pattern
element.value = value;
if (typeof updateInput === 'function') {
updateInput(element);
}
break;
default:
element.value = value;
// For text inputs and other types, use Forge pattern if available
if (typeof updateInput === 'function' && (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT')) {
updateInput(element);
} else {
this.triggerEvent(element, event);
}
}
},
// Update input using Gradio's expected event format (works with Forge and new Gradio)
updateGradioInput: function updateGradioInput(target) {
if (!target) return;
// Use the global updateInput if available (Forge/A1111)
if (typeof updateInput === 'function') {
updateInput(target);
}
// Also dispatch events manually to ensure Svelte/Gradio components update
// Input event with bubbles
let inputEvent = new Event("input", { bubbles: true, cancelable: true });
Object.defineProperty(inputEvent, "target", { value: target });
target.dispatchEvent(inputEvent);
// Change event
let changeEvent = new Event("change", { bubbles: true, cancelable: true });
target.dispatchEvent(changeEvent);
// For Svelte components, also try InputEvent
try {
let inputEvent2 = new InputEvent("input", {
bubbles: true,
cancelable: true,
inputType: "insertText",
data: target.value
});
target.dispatchEvent(inputEvent2);
} catch (e) {
// InputEvent might not be supported in all browsers
}
},
// Set value using native setter to bypass framework reactivity issues
setNativeValue: function setNativeValue(element, value) {
// Get the native value setter
const valueSetter = Object.getOwnPropertyDescriptor(element.__proto__, 'value')?.set ||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set ||
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
if (valueSetter) {
valueSetter.call(element, value);
} else {
element.value = value;
}
},
// Update a Gradio slider component - matches exact Forge pattern
updateGradioSlider: function updateGradioSlider(container, value) {
if (!container) return;
let numberInput = container.querySelector('input[type=number]');
let rangeInput = container.querySelector('input[type=range]');
// Use exact Forge pattern: set .value then call updateInput()
if (numberInput) {
numberInput.value = value;
if (typeof updateInput === 'function') {
updateInput(numberInput);
}
}
// Also update range for visual sync
if (rangeInput) {
rangeInput.value = value;
if (typeof updateInput === 'function') {
updateInput(rangeInput);
}
// Update visual slider fill
let min = parseFloat(rangeInput.min) || 0;
let max = parseFloat(rangeInput.max) || 100;
let val = parseFloat(value) || 0;
let percentage = ((val - min) / (max - min)) * 100;
rangeInput.style.backgroundSize = percentage + '% 100%';
}
},
onContentChange: function onContentChange(targetNode, func) {
const observer = new MutationObserver((mutationsList, observer) => {
@ -91,17 +188,22 @@ state.utils = {
setTimeout(() => {
state.utils.onContentChange(select, function (el) {
let selected = el.querySelector('span.single-select');
if (selected) {
store.set(id, selected.textContent);
} else {
// new gradio version...
let input = select.querySelector('input');
if (input) {
store.set(id, input.value);
}
// Use compatibility helper to get dropdown value
var value = state.utils.getDropdownValue(el);
if (value) {
store.set(id, value);
}
});
// Also listen to input events directly for Gradio 4.x
let input = select.querySelector('input');
if (input) {
input.addEventListener('change', function() {
if (this.value) {
store.set(id, this.value);
}
});
}
}, 150);
} catch (error) {
console.error('[state]: Error:', error);
@ -144,7 +246,8 @@ state.utils = {
}
}
state.utils.onContentChange(select, function (el) {
const selected = Array.from(el.querySelectorAll('.token > span')).map(item => item.textContent);
// Use compatibility helper to get multi-select values
const selected = state.utils.getMultiSelectValues(el);
store.set(id, selected);
});
} catch (error) {
@ -225,7 +328,167 @@ state.utils.html = {
const btn = document.createElement('button');
btn.innerHTML = text;
btn.onclick = onclick || function () {};
btn.className = 'gr-button gr-button-lg gr-button-primary';
// Support both old Gradio 3.x and new Gradio 4.x button classes
btn.className = state.utils.getButtonClass();
return btn;
},
// Get the appropriate button class based on Gradio version
getButtonClass: function() {
return state.utils.getButtonClass();
}
};
// Gradio version detection and compatibility helpers
state.utils.gradio = {
_version: null,
_detected: false,
// Detect Gradio version based on available DOM elements/classes
detectVersion: function() {
if (this._detected) return this._version;
var root = gradioApp();
// Check for Gradio 4.x indicators
if (root.querySelector('.gradio-container-4-')) {
this._version = 4;
} else if (root.querySelector('[class*="gradio-container-4"]')) {
this._version = 4;
} else if (root.querySelector('.svelte-')) {
// Gradio 4.x uses svelte classes
this._version = 4;
} else {
// Default to 3.x for older versions
this._version = 3;
}
this._detected = true;
state.logging.log('Detected Gradio version: ' + this._version + '.x');
return this._version;
},
isVersion4: function() {
return this.detectVersion() >= 4;
}
};
// Get button class with fallback support
state.utils.getButtonClass = function() {
var root = gradioApp();
// Try to find an existing button and copy its class
var existingBtn = root.querySelector('#quicksettings button');
if (existingBtn && existingBtn.className) {
return existingBtn.className;
}
// Fallback class names - try both old and new
if (state.utils.gradio.isVersion4()) {
return 'lg primary gradio-button svelte-cmf5ev';
}
return 'gr-button gr-button-lg gr-button-primary';
};
// Find dropdown elements with fallback selectors
state.utils.findDropdowns = function(container) {
container = container || gradioApp();
// Try multiple selectors for compatibility
var dropdowns = container.querySelectorAll('.gradio-dropdown');
if (!dropdowns.length) {
dropdowns = container.querySelectorAll('[data-testid="dropdown"]');
}
if (!dropdowns.length) {
dropdowns = container.querySelectorAll('.dropdown');
}
return dropdowns;
};
// Find accordion elements with fallback selectors
state.utils.findAccordions = function(container) {
container = container || gradioApp();
var accordions = container.querySelectorAll('.gradio-accordion');
if (!accordions.length) {
accordions = container.querySelectorAll('.accordion');
}
if (!accordions.length) {
accordions = container.querySelectorAll('[data-testid="accordion"]');
}
return accordions;
};
// Get selected value from dropdown with version compatibility
state.utils.getDropdownValue = function(select) {
if (!select) return null;
// Try new Gradio 4.x input method first
var input = select.querySelector('input');
if (input && input.value) {
return input.value;
}
// Try old Gradio 3.x span method
var selected = select.querySelector('span.single-select');
if (selected) {
return selected.textContent;
}
// Try other common patterns
var selectedOption = select.querySelector('.selected');
if (selectedOption) {
return selectedOption.textContent;
}
return null;
};
// Get multi-select values with version compatibility
state.utils.getMultiSelectValues = function(select) {
if (!select) return [];
// Try token pattern (common in both versions)
var tokens = select.querySelectorAll('.token > span, .token span:first-child');
if (tokens.length) {
return Array.from(tokens).map(item => item.textContent);
}
// Try secondary-wrap pattern (Gradio 4.x)
var secondary = select.querySelectorAll('.secondary-wrap .token');
if (secondary.length) {
return Array.from(secondary).map(item => item.textContent.trim());
}
// Try pill/tag pattern
var pills = select.querySelectorAll('.pill, .tag, [data-value]');
if (pills.length) {
return Array.from(pills).map(item => item.textContent || item.dataset.value);
}
return [];
};
// Check if accordion is open with version compatibility
state.utils.isAccordionOpen = function(accordion) {
if (!accordion) return false;
var labelWrap = accordion.querySelector('.label-wrap');
if (labelWrap) {
// Check for 'open' class (Forge/A1111 style)
if (labelWrap.classList.contains('open')) {
return true;
}
}
// Check for input-accordion-open class
if (accordion.classList.contains('input-accordion-open')) {
return true;
}
// Check icon rotation (older pattern)
var icon = accordion.querySelector('.transition, .icon');
if (icon) {
var transform = icon.style.transform || window.getComputedStyle(icon).transform;
if (transform && transform.indexOf('90') === -1) {
return true;
}
}
return false;
};

View File

@ -1,5 +1,5 @@
from fastapi import FastAPI, Body, HTTPException, Request, Response
from fastapi.responses import FileResponse
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse
import gradio as gr
import modules.shared as shared
@ -7,6 +7,11 @@ import modules.script_callbacks as script_callbacks
class StateApi():
"""
API endpoint for the State extension.
Provides configuration data to the frontend JavaScript.
Compatible with both AUTOMATIC1111 and Forge WebUI.
"""
BASE_PATH = '/state'
@ -21,11 +26,32 @@ class StateApi():
self.add_api_route('/config.json', self.get_config, methods=['GET'])
def get_config(self):
return FileResponse(shared.cmd_opts.ui_settings_file)
"""
Return the UI settings file containing state configuration.
Works with both A1111 and Forge which may have different settings locations.
"""
try:
# Try standard location first (works for both A1111 and Forge)
settings_file = getattr(shared.cmd_opts, 'ui_settings_file', None)
if settings_file:
return FileResponse(settings_file)
# Fallback: try to get settings from shared.opts
config = {
'state': getattr(shared.opts, 'state', []),
'state_txt2img': getattr(shared.opts, 'state_txt2img', []),
'state_img2img': getattr(shared.opts, 'state_img2img', []),
'state_extensions': getattr(shared.opts, 'state_extensions', []),
'state_ui': getattr(shared.opts, 'state_ui', []),
}
return JSONResponse(content=config)
except Exception as e:
print(f"[State] Error loading config: {e}")
return JSONResponse(content={})
try:
api = StateApi()
script_callbacks.on_app_started(api.start)
except:
pass
except Exception as e:
print(f"[State] Error initializing API: {e}")

View File

@ -1,18 +1,29 @@
"""
State extension settings for Stable Diffusion WebUI.
Compatible with both AUTOMATIC1111 and Forge WebUI.
"""
import gradio as gr
import modules.shared as shared
from modules import scripts
def on_ui_settings():
"""
Register the State extension settings in the WebUI settings page.
Uses CheckboxGroup for compatibility with both Gradio 3.x and 4.x.
"""
section = ("state", "State")
# Main elements (tabs)
shared.opts.add_option("state", shared.OptionInfo([], "Saved main elements", gr.CheckboxGroup, lambda: {
"choices": [
"tabs"
]
}, section=section))
# txt2img elements - includes Forge-specific options
shared.opts.add_option("state_txt2img", shared.OptionInfo([], "Saved elements from txt2img", gr.CheckboxGroup, lambda: {
"choices": [
"prompt",
@ -34,6 +45,7 @@ def on_ui_settings():
"hires_resize_x",
"hires_resize_y",
"hires_denoising_strength",
"hires_cfg_scale",
"refiner",
"refiner_checkpoint",
"refiner_switch",
@ -50,6 +62,7 @@ def on_ui_settings():
]
}, section=section))
# img2img elements
shared.opts.add_option("state_img2img", shared.OptionInfo([], "Saved elements from img2img", gr.CheckboxGroup, lambda: {
"choices": [
"prompt",
@ -82,6 +95,7 @@ def on_ui_settings():
]
}, section=section))
# Extension support - includes Forge built-in extensions
shared.opts.add_option("state_extensions", shared.OptionInfo([], "Saved elements from extensions", gr.CheckboxGroup, lambda: {
"choices": [
"control-net",
@ -91,6 +105,7 @@ def on_ui_settings():
]
}, section=section))
# UI buttons configuration
shared.opts.add_option("state_ui", shared.OptionInfo([
"Reset Button",
"Import Button",
@ -103,4 +118,8 @@ def on_ui_settings():
],
}, section=section))
try:
scripts.script_callbacks.on_ui_settings(on_ui_settings)
except Exception as e:
print(f"[State] Error registering settings: {e}")