Forge support - initial commi

pull/83/head
ilian.iliev 2025-11-23 19:12:59 +02:00
parent 3660174122
commit e1578a252c
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). 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 ## Development
**No build system** - This is a pure extension with vanilla JavaScript and Python. Files are loaded directly by the parent Stable Diffusion WebUI. **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`) - All code lives under `window.state` namespace (`state.core`, `state.store`, `state.utils`, `state.extensions`)
- Extension plugins register via `state.extensions[name] = { init: function() {} }` - 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) ### Backend (Python)
``` ```

View File

@ -14,6 +14,7 @@ state.core = (function () {
'hires_resize_x': 'hr_resize_x', 'hires_resize_x': 'hr_resize_x',
'hires_resize_y': 'hr_resize_y', 'hires_resize_y': 'hr_resize_y',
'hires_denoising_strength': 'denoising_strength', 'hires_denoising_strength': 'denoising_strength',
'hires_cfg_scale': 'hr_cfg',
'refiner_switch': 'switch_at', 'refiner_switch': 'switch_at',
'upscaler_2_visibility': 'extras_upscaler_2_visibility', 'upscaler_2_visibility': 'extras_upscaler_2_visibility',
'upscaler_scale_by_resize': 'extras_upscaling_resize', '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) { function restoreTabs(config) {
if (! config.hasSetting('tabs')) { if (! config.hasSetting('tabs')) {
return; return;
} }
const tabs = gradioApp().querySelectorAll('#tabs > div:first-child button'); const tabs = getTabButtons();
const value = store.get('tab'); const value = store.get('tab');
if (value) { if (value) {
@ -244,20 +279,23 @@ state.core = (function () {
} }
// Use this when onUiTabChange is fixed // Use this when onUiTabChange is fixed
// onUiTabChange(function () { // onUiTabChange(function () {
// store.set('tab', gradioApp().querySelector('#tabs .tab-nav button.selected').textContent); // store.set('tab', getSelectedTabButton()?.textContent);
// }); // });
bindTabClickEvents(); bindTabClickEvents();
} }
function bindTabClickEvents() { function bindTabClickEvents() {
Array.from(gradioApp().querySelectorAll('#tabs .tab-nav button')).forEach(tab => { Array.from(getTabButtons()).forEach(tab => {
tab.removeEventListener('click', storeTab); tab.removeEventListener('click', storeTab);
tab.addEventListener('click', storeTab); tab.addEventListener('click', storeTab);
}); });
} }
function 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... bindTabClickEvents(); // dirty hack here...
} }
@ -288,6 +326,12 @@ state.core = (function () {
return; 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) { let forEach = function (action) {
events.forEach(function(event) { events.forEach(function(event) {
elements.forEach(function (element) { elements.forEach(function (element) {
@ -296,6 +340,7 @@ state.core = (function () {
}); });
}; };
// Attach event listeners to all elements
forEach(function (event) { forEach(function (event) {
this.addEventListener(event, function () { this.addEventListener(event, function () {
let value = this.value; let value = this.value;
@ -306,17 +351,23 @@ state.core = (function () {
}); });
}); });
// Special handling for seed buttons
if (id.indexOf('seed') > -1) {
TABS.forEach(tab => { TABS.forEach(tab => {
const seedInput = gradioApp().querySelector(`#${tab}_seed input`); const seedInput = gradioApp().querySelector(`#${tab}_seed input[type="number"]`);
['random_seed', 'reuse_seed'].forEach(id => { if (!seedInput) return;
const btn = gradioApp().querySelector(`#${tab}_${id}`); ['random_seed', 'reuse_seed'].forEach(btnId => {
const btn = gradioApp().querySelector(`#${tab}_${btnId}`);
if (btn) {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
setTimeout(() => { setTimeout(() => {
state.utils.triggerEvent(seedInput, 'change'); state.utils.triggerEvent(seedInput, 'change');
}, 100); }, 100);
}); });
}
}); });
}); });
}
let value = store.get(id); let value = store.get(id);
@ -324,10 +375,68 @@ state.core = (function () {
return; return;
} }
forEach(function (event) { // Delay restoration to ensure UI and updateInput are ready
state.utils.setValue(this, value, event); // 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) { function handleSavedSelects(id, duplicateIds) {
if (duplicateIds) { if (duplicateIds) {
@ -353,15 +462,29 @@ state.core = (function () {
let selector = duplicateIds ? `[id="${id}"]` : `#${id}`; 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) { elements.forEach(function (element) {
if (store.get(id) === 'true') { if (store.get(id) === 'true') {
state.utils.clickToggleMenu(element); state.utils.clickToggleMenu(element);
} }
element.addEventListener('click', function () { element.addEventListener('click', function () {
let classList = Array.from(this.parentNode.classList); var parent = this.parentNode;
store.set(id, classList.indexOf('input-accordion-open') > -1); // 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 = []; let cnTabs = [];
function bindTabEvents() { 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 tabs.forEach(tab => { // dirty hack here
tab.removeEventListener('click', onTabClick); tab.removeEventListener('click', onTabClick);
tab.addEventListener('click', onTabClick); tab.addEventListener('click', onTabClick);
@ -36,6 +43,8 @@ state.extensions['adetailer'] = (function () {
} }
function handleCheckbox(checkbox, id) { function handleCheckbox(checkbox, id) {
if (!checkbox) return;
let value = store.get(id); let value = store.get(id);
if (value) { if (value) {
state.utils.setValue(checkbox, value, 'change'); state.utils.setValue(checkbox, value, 'change');
@ -81,7 +90,8 @@ state.extensions['adetailer'] = (function () {
} }
function handleSelects(container, container_idx) { 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) { selects.forEach(function (select, idx) {
state.utils.handleSelect(select, `ad-tab-${container_idx}-select-${idx}`, store); state.utils.handleSelect(select, `ad-tab-${container_idx}-select-${idx}`, store);
}); });
@ -107,28 +117,49 @@ state.extensions['adetailer'] = (function () {
} }
function handleDropdown(dropdown, id) { function handleDropdown(dropdown, id) {
if (!dropdown) return;
let value = store.get(id); let value = store.get(id);
if (value && value === 'true') { if (value && value === 'true') {
state.utils.triggerEvent(dropdown, 'click'); state.utils.triggerEvent(dropdown, 'click');
} }
dropdown.addEventListener('click', function () { dropdown.addEventListener('click', function () {
let span = this.querySelector('.transition, .icon'); // Use multiple methods to check open state for compatibility
store.set(id, span.style.transform !== 'rotate(90deg)'); 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) { function handleDropdowns(container, container_idx) {
let dropdowns = container.querySelectorAll('.gradio-accordion .label-wrap'); // Use compatibility helper to find accordions
dropdowns.forEach(function (dropdown, idx) { let accordions = state.utils.findAccordions(container);
handleDropdown(dropdown, `ad-tab-${container_idx}-dropdown-${idx}`); accordions.forEach(function (accordion, idx) {
let labelWrap = accordion.querySelector('.label-wrap');
if (labelWrap) {
handleDropdown(labelWrap, `ad-tab-${container_idx}-dropdown-${idx}`);
}
}); });
} }
function load() { function load() {
setTimeout(function () { setTimeout(function () {
handleDropdown(container.querySelector('#script_txt2img_adetailer_ad_main_accordion > .label-wrap'), 'ad-dropdown-main'); // Try multiple selectors for the main accordion
handleCheckbox(container.querySelector('#script_txt2img_adetailer_ad_enable > label > input'), 'ad-checkbox-enable'); 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 }) => { cnTabs.forEach(({ container, container_idx }) => {
handleTabs(container, container_idx); handleTabs(container, container_idx);
handleTextboxes(container, container_idx); handleTextboxes(container, container_idx);
@ -143,14 +174,30 @@ state.extensions['adetailer'] = (function () {
function init() { function init() {
// Try multiple selectors for ADetailer container (Forge vs A1111 compatibility)
container = gradioApp().getElementById('script_txt2img_adetailer_ad_main_accordion'); 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) { if (!container) {
state.logging.log('ADetailer extension not found');
return; return;
} }
// Try multiple selectors for tabs
let tabs = container.querySelectorAll('.tabitem'); 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) { if (tabs.length) {
cnTabs = []; cnTabs = [];
@ -162,7 +209,8 @@ state.extensions['adetailer'] = (function () {
}); });
} else { } else {
cnTabs = [{ cnTabs = [{
container: container container: container,
container_idx: 0
}]; }];
} }

View File

@ -11,7 +11,17 @@ function ControlNetTabContext(tabName, container) {
this.tabElements = []; this.tabElements = [];
this.cnTabs = []; this.cnTabs = [];
// Try multiple selectors for compatibility with different Gradio/Forge versions
let tabs = this.container.querySelectorAll(':scope > div > div > .tabs > .tabitem'); 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) { if (tabs.length) {
tabs.forEach((tabContainer, i) => { tabs.forEach((tabContainer, i) => {
@ -21,10 +31,10 @@ function ControlNetTabContext(tabName, container) {
}); });
}); });
} else { } else {
this.cnTabs.push[{ this.cnTabs.push({
container: container, 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 => { 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 => { elements.forEach(element => {
if (context.store.get(id) === 'true') { if (context.store.get(id) === 'true') {
@ -46,8 +60,11 @@ state.extensions['control-net'] = (function () {
load(); load();
} }
element.addEventListener('click', function () { element.addEventListener('click', function () {
let classList = Array.from(this.classList); // Check for open state using multiple methods for compatibility
context.store.set(id, classList.indexOf('open') > -1); let isOpen = this.classList.contains('open') ||
this.parentNode.classList.contains('open') ||
state.utils.isAccordionOpen(this.parentNode);
context.store.set(id, isOpen);
load(); load();
}); });
}); });
@ -56,7 +73,18 @@ state.extensions['control-net'] = (function () {
function bindTabEvents() { function bindTabEvents() {
contexts.forEach(context => { 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() { function onTabClick() {
context.store.set('tab', this.textContent); context.store.set('tab', this.textContent);
bindTabEvents(); 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() { function handleCheckboxes() {
handleContext((container, store) => { handleContext((container, store) => {
let checkboxes = container.querySelectorAll('input[type="checkbox"]'); let checkboxes = container.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(function (checkbox) { checkboxes.forEach(function (checkbox, idx) {
let label = checkbox.nextElementSibling; let labelText = getCheckboxLabel(checkbox);
let id = state.utils.txtToId(label.textContent); // Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `checkbox-${idx}`;
let value = store.get(id); let value = store.get(id);
if (value) { if (value) {
state.utils.setValue(checkbox, value, 'change'); state.utils.setValue(checkbox, value, 'change');
@ -111,8 +211,12 @@ state.extensions['control-net'] = (function () {
function handleSelects() { function handleSelects() {
handleContext((container, store) => { handleContext((container, store) => {
container.querySelectorAll('.gradio-dropdown').forEach(select => { // Use compatibility helper to find dropdowns
let id = state.utils.txtToId(select.querySelector('label').firstChild.textContent); 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); let value = store.get(id);
state.utils.handleSelect(select, id, store); state.utils.handleSelect(select, id, store);
if (id === 'preprocessor' && value && value.toLowerCase() !== 'none') { 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() { function handleSliders() {
handleContext((container, store) => { handleContext((container, store) => {
let sliders = container.querySelectorAll('input[type="range"]'); let sliders = container.querySelectorAll('input[type="range"]');
sliders.forEach(function (slider) { sliders.forEach(function (slider, idx) {
let label = slider.previousElementSibling.querySelector('label span'); let labelText = getSliderLabel(slider);
let id = state.utils.txtToId(label.textContent); // Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `slider-${idx}`;
let value = store.get(id); let value = store.get(id);
if (value) { if (value) {
state.utils.setValue(slider, value, 'change'); state.utils.setValue(slider, value, 'change');
@ -142,10 +298,11 @@ state.extensions['control-net'] = (function () {
function handleRadioButtons() { function handleRadioButtons() {
handleContext((container, store) => { handleContext((container, store) => {
let fieldsets = container.querySelectorAll('fieldset'); let fieldsets = container.querySelectorAll('fieldset');
fieldsets.forEach(function (fieldset) { fieldsets.forEach(function (fieldset, idx) {
let label = fieldset.firstChild.nextElementSibling;
let radios = fieldset.querySelectorAll('input[type="radio"]'); 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); let value = store.get(id);
if (value) { if (value) {
radios.forEach(function (radio) { radios.forEach(function (radio) {
@ -164,9 +321,10 @@ state.extensions['control-net'] = (function () {
function handleTextareas() { function handleTextareas() {
handleContext((container, store) => { handleContext((container, store) => {
let textareas = container.querySelectorAll('textarea'); let textareas = container.querySelectorAll('textarea');
textareas.forEach(function (textarea) { textareas.forEach(function (textarea, idx) {
let label = textarea.previousElementSibling; let labelText = getTextareaLabel(textarea);
let id = state.utils.txtToId(label.textContent); // Use index-based fallback if no label found
let id = labelText ? state.utils.txtToId(labelText) : `textarea-${idx}`;
let value = store.get(id); let value = store.get(id);
if (value) { if (value) {
state.utils.setValue(textarea, value, 'change'); state.utils.setValue(textarea, value, 'change');
@ -191,14 +349,32 @@ state.extensions['control-net'] = (function () {
function init() { function init() {
// Try multiple selectors for ControlNet container (Forge vs A1111 compatibility)
let elements = gradioApp().querySelectorAll('#controlnet'); 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) { if (!elements.length) {
state.logging.log('ControlNet extension not found');
return; return;
} }
// Handle both single container and separate txt2img/img2img containers
if (elements.length >= 2) {
contexts[0] = new ControlNetTabContext('txt2img', elements[0]); contexts[0] = new ControlNetTabContext('txt2img', elements[0]);
contexts[1] = new ControlNetTabContext('img2img', elements[1]); contexts[1] = new ControlNetTabContext('img2img', elements[1]);
} else if (elements.length === 1) {
// Single container mode
contexts[0] = new ControlNetTabContext('main', elements[0]);
}
handleToggle(); handleToggle();
load(); load();

View File

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

View File

@ -50,7 +50,8 @@ state.extensions['multidiffusion'] = (function () {
} }
function handleSelects(container, name) { 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) { selects.forEach(function (select, idx) {
state.utils.handleSelect(select, `md-${name}-select-${idx}`, store); state.utils.handleSelect(select, `md-${name}-select-${idx}`, store);
}); });
@ -76,17 +77,24 @@ state.extensions['multidiffusion'] = (function () {
} }
function handleDropdowns(container, name) { function handleDropdowns(container, name) {
let dropdowns = container.querySelectorAll('.gradio-accordion .label-wrap'); // Use compatibility helper to find accordions
dropdowns.forEach(function (dropdown, idx) { 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 id = `md-${name}-dropdown-${idx}`;
let value = store.get(id); let value = store.get(id);
if (value && value === 'true') { if (value && value === 'true') {
state.utils.triggerEvent(dropdown, 'click'); state.utils.triggerEvent(labelWrap, 'click');
} }
dropdown.addEventListener('click', function () { labelWrap.addEventListener('click', function () {
let span = this.querySelector('.transition, .icon'); // Use multiple methods to check open state for compatibility
store.set(id, span.style.transform !== 'rotate(90deg)'); 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() { function init() {
// Try to find Tiled Diffusion/VAE containers by text content
let spanTags = gradioApp().getElementsByTagName("span"); let spanTags = gradioApp().getElementsByTagName("span");
for (var i = 0; i < spanTags.length; i++) { for (var i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent == 'Tiled Diffusion') { let text = spanTags[i].textContent.trim();
containers.push({container: spanTags[i].parentElement.parentElement,name: 'diffusion'}); 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'); store = new state.Store('ext-multidiffusion');
if (!containers.length) { if (!containers.length) {
state.logging.log('Multidiffusion/Tiled VAE extension not found');
return; return;
} }

View File

@ -5,15 +5,45 @@ state.logging = {
name: 'state', 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); 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); 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); 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; element.checked = false;
} }
break; 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: default:
element.value = value; 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); 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) { onContentChange: function onContentChange(targetNode, func) {
const observer = new MutationObserver((mutationsList, observer) => { const observer = new MutationObserver((mutationsList, observer) => {
@ -91,17 +188,22 @@ state.utils = {
setTimeout(() => { setTimeout(() => {
state.utils.onContentChange(select, function (el) { state.utils.onContentChange(select, function (el) {
let selected = el.querySelector('span.single-select'); // Use compatibility helper to get dropdown value
if (selected) { var value = state.utils.getDropdownValue(el);
store.set(id, selected.textContent); if (value) {
} else { store.set(id, value);
// new gradio version...
let input = select.querySelector('input');
if (input) {
store.set(id, input.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); }, 150);
} catch (error) { } catch (error) {
console.error('[state]: Error:', error); console.error('[state]: Error:', error);
@ -144,7 +246,8 @@ state.utils = {
} }
} }
state.utils.onContentChange(select, function (el) { 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); store.set(id, selected);
}); });
} catch (error) { } catch (error) {
@ -225,7 +328,167 @@ state.utils.html = {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.innerHTML = text; btn.innerHTML = text;
btn.onclick = onclick || function () {}; 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; 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 import FastAPI
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, JSONResponse
import gradio as gr import gradio as gr
import modules.shared as shared import modules.shared as shared
@ -7,6 +7,11 @@ import modules.script_callbacks as script_callbacks
class StateApi(): 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' BASE_PATH = '/state'
@ -21,11 +26,32 @@ class StateApi():
self.add_api_route('/config.json', self.get_config, methods=['GET']) self.add_api_route('/config.json', self.get_config, methods=['GET'])
def get_config(self): 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: try:
api = StateApi() api = StateApi()
script_callbacks.on_app_started(api.start) script_callbacks.on_app_started(api.start)
except: except Exception as e:
pass 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 gradio as gr
import modules.shared as shared import modules.shared as shared
from modules import scripts from modules import scripts
def on_ui_settings(): 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") section = ("state", "State")
# Main elements (tabs)
shared.opts.add_option("state", shared.OptionInfo([], "Saved main elements", gr.CheckboxGroup, lambda: { shared.opts.add_option("state", shared.OptionInfo([], "Saved main elements", gr.CheckboxGroup, lambda: {
"choices": [ "choices": [
"tabs" "tabs"
] ]
}, section=section)) }, section=section))
# txt2img elements - includes Forge-specific options
shared.opts.add_option("state_txt2img", shared.OptionInfo([], "Saved elements from txt2img", gr.CheckboxGroup, lambda: { shared.opts.add_option("state_txt2img", shared.OptionInfo([], "Saved elements from txt2img", gr.CheckboxGroup, lambda: {
"choices": [ "choices": [
"prompt", "prompt",
@ -34,6 +45,7 @@ def on_ui_settings():
"hires_resize_x", "hires_resize_x",
"hires_resize_y", "hires_resize_y",
"hires_denoising_strength", "hires_denoising_strength",
"hires_cfg_scale",
"refiner", "refiner",
"refiner_checkpoint", "refiner_checkpoint",
"refiner_switch", "refiner_switch",
@ -50,6 +62,7 @@ def on_ui_settings():
] ]
}, section=section)) }, section=section))
# img2img elements
shared.opts.add_option("state_img2img", shared.OptionInfo([], "Saved elements from img2img", gr.CheckboxGroup, lambda: { shared.opts.add_option("state_img2img", shared.OptionInfo([], "Saved elements from img2img", gr.CheckboxGroup, lambda: {
"choices": [ "choices": [
"prompt", "prompt",
@ -82,6 +95,7 @@ def on_ui_settings():
] ]
}, section=section)) }, section=section))
# Extension support - includes Forge built-in extensions
shared.opts.add_option("state_extensions", shared.OptionInfo([], "Saved elements from extensions", gr.CheckboxGroup, lambda: { shared.opts.add_option("state_extensions", shared.OptionInfo([], "Saved elements from extensions", gr.CheckboxGroup, lambda: {
"choices": [ "choices": [
"control-net", "control-net",
@ -91,6 +105,7 @@ def on_ui_settings():
] ]
}, section=section)) }, section=section))
# UI buttons configuration
shared.opts.add_option("state_ui", shared.OptionInfo([ shared.opts.add_option("state_ui", shared.OptionInfo([
"Reset Button", "Reset Button",
"Import Button", "Import Button",
@ -103,4 +118,8 @@ def on_ui_settings():
], ],
}, section=section)) }, section=section))
try:
scripts.script_callbacks.on_ui_settings(on_ui_settings) scripts.script_callbacks.on_ui_settings(on_ui_settings)
except Exception as e:
print(f"[State] Error registering settings: {e}")