Compare commits

...

13 Commits

Author SHA1 Message Date
Ilian Iliev 7638caee79
Merge pull request #85 from Matthew-X/mask-regional-prompter
Mask regional prompter addition
2025-12-28 23:05:43 +02:00
Matthew-X c32cdaa4df Fixed issue with regional prompt state save. 2025-12-27 02:40:07 +13:00
Matthew-X 25e519ef05 fix for data retreival. 2025-12-26 17:34:51 +13:00
Matthew-X 3212cd5d91 Add mask-regional-prompter state extension support
Introduces JavaScript logic to save and restore state for the sd-webui-mask-regional-prompter extension, including mask data, prompts, and tool settings for both t2i and i2i tabs. Also registers the extension in the Python state settings configuration.
2025-12-26 12:06:45 +13:00
Ilian Iliev d59fe769f5
Merge pull request #84 from ilian6806/develop
Forge support
2025-11-23 19:43:10 +02:00
Ilian Iliev 13f224c02f
Merge pull request #83 from ilian6806/forge
Forge support
2025-11-23 19:40:46 +02:00
ilian.iliev e1578a252c Forge support - initial commi 2025-11-23 19:12:59 +02:00
ilian.iliev 3660174122 Claude init 2025-11-23 01:32:57 +02:00
Ilian Iliev 8126c49f62
Merge pull request #77 from ilian6806/develop
Develop
2024-10-07 21:38:31 +03:00
ilian.iliev ef4ca45118 Fixed ControlNet extension for saving batch dir 2024-10-07 21:37:32 +03:00
ilian.iliev bd957412eb Fixed ControlNet extension for separate tabs 2024-10-07 21:29:00 +03:00
Ilian Iliev c21e914102
Merge pull request #76 from ilian6806/develop
Fixed ControlNet extension for new gradio
2024-10-07 00:31:22 +03:00
ilian.iliev 999b4b3b0d Fixed ControlNet extension for new gradio 2024-10-07 00:00:44 +03:00
11 changed files with 1252 additions and 140 deletions

88
CLAUDE.md Normal file
View File

@ -0,0 +1,88 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
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.
- Modify JavaScript/Python files directly
- Reload the webui to test changes
- Place extension in parent webui's `extensions/` directory
## Architecture
### Frontend (JavaScript)
```
state.app.js → Entry point, calls state.core.init() on DOMContentLoaded
state.core.js → Main module: element mappings, initialization, UI buttons
state.store.js → LocalStorage wrapper with 'state-' prefix
state.utils.js → DOM manipulation, event handling, file operations
state.constants.js → Constants (LS_PREFIX)
state.loggings.js → Console logging utilities
state.ext.*.js → Extension-specific handlers (ControlNet, ADetailer, etc.)
```
**Key patterns:**
- IIFE modules prevent global namespace pollution
- 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)
```
scripts/state_api.py → FastAPI endpoint: GET /state/config.json
scripts/state_settings.py → Gradio settings UI (checkbox groups for element preservation)
```
### Data Flow
1. `state.core.init()` fetches `/state/config.json` from Python backend
2. Config specifies which elements to preserve per tab (txt2img, img2img)
3. Event listeners attached to elements → changes saved to localStorage
4. On page load, values restored from localStorage to elements
### Element Categories in state.core.js
- `INPUTS`: Text inputs, textareas, sliders (prompt, steps, seed, etc.)
- `SELECTS`: Dropdowns (sampling, scheduler, upscaler, etc.)
- `MULTI_SELECTS`: Multi-select dropdowns (styles)
- `TOGGLE_BUTTONS`: Accordion toggles (hires_fix, refiner, tiled_diffusion)
## Adding Support for New UI Elements
1. Add element mapping in `state.core.js` (INPUTS, SELECTS, etc.)
2. Implement or reuse handler function (`handleSavedInput`, `handleSavedSelects`, etc.)
3. Add checkbox option in `state_settings.py`
## Adding Support for New Extensions
1. Create `javascript/state.ext.{name}.js`
2. Implement IIFE module returning `{ init }` function
3. Register in `state_settings.py` as checkbox option
4. Reference `state.ext.control-net.js` for implementation pattern
## Git Workflow
- Feature branches from `develop`
- PRs target `develop` branch
- Main branch: `main`

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 () {
});
});
TABS.forEach(tab => {
const seedInput = gradioApp().querySelector(`#${tab}_seed input`);
['random_seed', 'reuse_seed'].forEach(id => {
const btn = gradioApp().querySelector(`#${tab}_${id}`);
btn.addEventListener('click', () => {
setTimeout(() => {
state.utils.triggerEvent(seedInput, 'change');
}, 100);
// Special handling for seed buttons
if (id.indexOf('seed') > -1) {
TABS.forEach(tab => {
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,9 +375,67 @@ 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) {
@ -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"]');
}
if (! container) {
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

@ -2,60 +2,202 @@ window.state = window.state || {};
window.state.extensions = window.state.extensions || {};
state = window.state;
function ControlNetTabContext(tabName, container) {
this.tabName = tabName;
this.container = container;
this.store = new state.Store(`ext-control-net-${this.tabName}`);
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) => {
this.cnTabs.push({
container: tabContainer,
store: new state.Store(`ext-control-net-${this.tabName}-${i}`)
});
});
} else {
this.cnTabs.push({
container: container,
store: new state.Store(`ext-control-net-${this.tabName}-0`)
});
}
}
state.extensions['control-net'] = (function () {
let container = null;
let store = null;
let cnTabs = [];
let contexts = [];
function handleToggle() {
let value = store.get('toggled');
let toggleBtn = container.querySelector('div.cursor-pointer, .label-wrap');
if (value && value === 'true') {
state.utils.triggerEvent(toggleBtn, 'click');
load();
}
toggleBtn.addEventListener('click', function () {
let span = this.querySelector('.transition, .icon');
store.set('toggled', span.style.transform !== 'rotate(90deg)');
load();
const id = 'toggled';
contexts.forEach(context => {
// 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') {
state.utils.clickToggleMenu(element);
load();
}
element.addEventListener('click', function () {
// 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();
});
});
});
}
function bindTabEvents() {
const tabs = container.querySelectorAll('.tabs > div > button');
tabs.forEach(tab => { // dirty hack here
tab.removeEventListener('click', onTabClick);
tab.addEventListener('click', onTabClick);
contexts.forEach(context => {
// 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();
}
tabs.forEach(tab => { // dirty hack here
tab.removeEventListener('click', onTabClick);
tab.addEventListener('click', onTabClick);
});
context.tabElements = tabs;
});
return tabs;
}
function handleTabs() {
let tabs = bindTabEvents();
let value = store.get('tab');
if (value) {
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].textContent === value) {
state.utils.triggerEvent(tabs[i], 'click');
break;
bindTabEvents();
contexts.forEach(context => {
let value = context.store.get('tab');
if (value) {
for (var i = 0; i < context.tabElements.length; i++) {
if (context.tabElements[i].textContent === value) {
state.utils.triggerEvent(context.tabElements[i], 'click');
break;
}
}
}
}
});
}
function onTabClick() {
store.set('tab', this.textContent);
bindTabEvents();
function handleContext(action) {
contexts.forEach(context => {
context.cnTabs.forEach(({ container, store }) => {
action(container, store);
});
});
}
// 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() {
cnTabs.forEach(({ container, store }) => {
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');
@ -68,9 +210,13 @@ state.extensions['control-net'] = (function () {
}
function handleSelects() {
cnTabs.forEach(({ container, store }) => {
container.querySelectorAll('.gradio-dropdown').forEach(select => {
let id = state.utils.txtToId(select.querySelector('label').firstChild.textContent);
handleContext((container, store) => {
// 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') {
@ -80,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() {
cnTabs.forEach(({ container, store }) => {
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');
@ -98,12 +296,13 @@ state.extensions['control-net'] = (function () {
}
function handleRadioButtons() {
cnTabs.forEach(({ container, store }) => {
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) {
@ -119,6 +318,24 @@ state.extensions['control-net'] = (function () {
});
}
function handleTextareas() {
handleContext((container, store) => {
let textareas = container.querySelectorAll('textarea');
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');
}
textarea.addEventListener('change', function () {
store.set(id, this.value);
});
});
});
}
function load() {
setTimeout(function () {
handleTabs();
@ -126,33 +343,37 @@ state.extensions['control-net'] = (function () {
handleSelects();
handleSliders();
handleRadioButtons();
handleTextareas();
}, 500);
}
function init() {
container = gradioApp().getElementById('controlnet');
store = new state.Store('ext-control-net');
// 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 (! container) {
if (!elements.length) {
state.logging.log('ControlNet extension not found');
return;
}
let tabs = container.querySelectorAll('.tabitem');
if (tabs.length) {
cnTabs = [];
tabs.forEach((tabContainer, i) => {
cnTabs.push({
container: tabContainer,
store: new state.Store('ext-control-net-' + i)
});
});
} else {
cnTabs = [{
container: container,
store: new state.Store('ext-control-net-0')
}];
// 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();

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) {
if (!container) {
state.logging.log('Dynamic Prompting extension not found');
return;
}

View File

@ -0,0 +1,238 @@
/**
* State Extension - Mask Regional Prompter Support
* Saves and restores state for sd-webui-mask-regional-prompter
*/
window.state = window.state || {};
window.state.extensions = window.state.extensions || {};
state = window.state;
state.extensions['mask-regional-prompter'] = (function () {
const TABS = ['t2i', 'i2i'];
// Elements to save/restore for each tab
const ELEMENTS = {
// Dimensions
'width': { selector: '#mrp_width_{tab} input', type: 'number' },
'height': { selector: '#mrp_height_{tab} input', type: 'number' },
// Tool settings
'brush_size': { selector: '#mrp_brush_size_{tab} input', type: 'number' },
'zoom_level': { selector: '#mrp_zoom_level_{tab} input', type: 'number' },
'layer_opacity': { selector: '#mrp_layer_opacity_{tab} input', type: 'number' },
// Prompts
'base_prompt': { selector: '#mrp_base_prompt_{tab} textarea', type: 'text' },
'base_neg_prompt': { selector: '#mrp_base_neg_prompt_{tab} textarea', type: 'text' },
// Layer data (JSON containing layer images)
'layer_data': { selector: '#mrp_layer_data_{tab} textarea', type: 'text', isData: true },
// Prompts dump (JSON containing layer prompts)
'prompts_dump': { selector: '#mrp_prompts_dump_{tab} textarea', type: 'text', isData: true },
// Composite mask image (fallback if layer_data unavailable)
'mask_data': { selector: '#mrp_mask_data_{tab} textarea', type: 'text', isData: true },
// Base image data (image user uploaded/dropped onto editor)
'base_image_data': { selector: '#mrp_base_image_data_{tab} textarea', type: 'text', isData: true }
};
let stores = {};
function getStore(tab) {
if (!stores[tab]) {
stores[tab] = new state.Store(`ext-mrp-${tab}`);
}
return stores[tab];
}
function getElement(selector, tab) {
const actualSelector = selector.replace('{tab}', tab);
return document.querySelector(actualSelector);
}
function saveElement(key, config, tab) {
const el = getElement(config.selector, tab);
if (!el) return;
const store = getStore(tab);
// Save current value
const handler = function () {
store.set(key, this.value);
state.logging.log(`[MRP] Saved ${key} for ${tab}`);
};
// Attach event listener
el.addEventListener('change', handler);
el.addEventListener('input', state.utils.debounce(handler.bind(el), 500));
// For data fields, also listen for programmatic changes
if (config.isData) {
const observer = new MutationObserver(() => {
store.set(key, el.value);
});
// Observe value attribute changes
observer.observe(el, { attributes: true, attributeFilter: ['value'] });
// Also poll for value changes (Gradio sometimes updates value directly)
setInterval(() => {
const currentValue = el.value;
const storedValue = store.get(key);
// Save if: value has changed AND (it's non-trivial OR we're clearing a non-empty stored value)
if (currentValue !== storedValue) {
if (currentValue.length > 10 || (currentValue === '' && storedValue && storedValue.length > 0)) {
store.set(key, currentValue);
}
}
}, 2000);
}
}
function restoreElement(key, config, tab) {
const el = getElement(config.selector, tab);
if (!el) return;
const store = getStore(tab);
const value = store.get(key);
if (!value) return;
// Restore value
el.value = value;
// Trigger appropriate events for Gradio to pick up
if (config.type === 'number') {
if (typeof updateInput === 'function') {
updateInput(el);
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
el.dispatchEvent(new Event('input', { bubbles: true }));
}
state.logging.log(`[MRP] Restored ${key} for ${tab}: ${value.substring(0, 50)}...`);
}
function restoreMaskEditor(tab) {
const store = getStore(tab);
const maskData = store.get('mask_data');
const layerData = store.get('layer_data');
const promptsDump = store.get('prompts_dump');
const baseImageData = store.get('base_image_data');
if (!maskData && !layerData && !baseImageData) {
state.logging.log(`[MRP] No mask data to restore for ${tab}`);
return;
}
// Wait for MaskEditorAPI to be available
const waitForAPI = setInterval(() => {
if (window.MaskEditorAPI) {
clearInterval(waitForAPI);
// Ensure editor is initialized
const editor = window.MaskEditors && window.MaskEditors[tab];
if (!editor) {
// Initialize the editor first
window.MaskEditorAPI.init(tab);
}
// Wait a bit for initialization, then load data
setTimeout(() => {
state.logging.log(`[MRP] Restoring mask data for ${tab}`);
const editorInstance = window.MaskEditors[tab];
if (!editorInstance) {
state.logging.warn(`[MRP] Editor instance not found for ${tab}`);
return;
}
// Load base image first (if available)
if (baseImageData) {
state.logging.log(`[MRP] Restoring base image for ${tab}`);
editorInstance.setBaseImage(baseImageData);
}
// Load the mask and layer data (after a small delay to allow base image to load)
setTimeout(() => {
if (maskData || layerData) {
window.MaskEditorAPI.loadMaskData(tab, maskData, layerData);
}
// Restore layer prompts
if (promptsDump) {
try {
const prompts = JSON.parse(promptsDump);
if (editorInstance) {
editorInstance.layerPrompts = prompts;
editorInstance.syncPromptFields();
}
} catch (e) {
state.logging.warn('[MRP] Failed to parse prompts dump: ' + e);
}
}
state.logging.log(`[MRP] Mask editor restored for ${tab}`);
}, baseImageData ? 500 : 0);
}, 1000);
}
}, 500);
// Timeout after 30 seconds
setTimeout(() => clearInterval(waitForAPI), 30000);
}
function handleTab(tab) {
// First restore non-data elements
for (const [key, config] of Object.entries(ELEMENTS)) {
if (!config.isData) {
restoreElement(key, config, tab);
}
}
// Restore the mask editor (async operation)
restoreMaskEditor(tab);
// Setup save handlers for all elements
for (const [key, config] of Object.entries(ELEMENTS)) {
saveElement(key, config, tab);
}
}
function init() {
// Check if MRP extension elements exist
const mrpContainer = document.querySelector('#mrp_canvas_t2i, #mrp_canvas_i2i');
if (!mrpContainer) {
state.logging.log('[MRP] Mask Regional Prompter extension not found');
return;
}
state.logging.log('[MRP] Initializing Mask Regional Prompter state support');
// Handle each tab
TABS.forEach(tab => {
// Wait for the MRP elements to be fully loaded
const checkElements = setInterval(() => {
const canvas = document.getElementById(`mrp_canvas_${tab}`);
const widthInput = document.querySelector(`#mrp_width_${tab} input`);
if (canvas && widthInput) {
clearInterval(checkElements);
state.logging.log(`[MRP] Elements found for ${tab}, initializing...`);
// Delay to ensure Gradio is fully ready
setTimeout(() => handleTab(tab), 1500);
}
}, 500);
// Timeout after 30 seconds
setTimeout(() => clearInterval(checkElements), 30000);
});
}
return { init };
}());

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 (spanTags[i].textContent == 'Tiled VAE') {
containers.push({container: spanTags[i].parentElement.parentElement,name: 'vae'});
break;
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'});
}
}
store = new state.Store('ext-multidiffusion');
if (! containers.length) {
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) {
console.log(`[${this.name}]: `, 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) {
console.error(`[${this.name}]: `, 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) {
console.warn(`[${this.name}]: `, 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,9 +46,106 @@ 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;
this.triggerEvent(element, event);
// 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) {
@ -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,15 +95,18 @@ 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",
"adetailer",
"multidiffusion",
"dynamic prompting"
"dynamic prompting",
"mask-regional-prompter"
]
}, section=section))
# UI buttons configuration
shared.opts.add_option("state_ui", shared.OptionInfo([
"Reset Button",
"Import Button",
@ -103,4 +119,8 @@ def on_ui_settings():
],
}, section=section))
scripts.script_callbacks.on_ui_settings(on_ui_settings)
try:
scripts.script_callbacks.on_ui_settings(on_ui_settings)
except Exception as e:
print(f"[State] Error registering settings: {e}")