Forge support - initial commi
parent
3660174122
commit
e1578a252c
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -6,6 +6,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
This is a Stable Diffusion WebUI extension that preserves UI parameters (inputs, sliders, checkboxes, etc.) after page reload. It uses localStorage for persistence and supports multiple SD extensions (ControlNet, ADetailer, Dynamic Prompting, Multidiffusion/Tiled VAE).
|
||||
|
||||
**Compatible with:**
|
||||
- AUTOMATIC1111 Stable Diffusion WebUI
|
||||
- Stable Diffusion WebUI Forge
|
||||
- Gradio 3.x and 4.x
|
||||
|
||||
## Development
|
||||
|
||||
**No build system** - This is a pure extension with vanilla JavaScript and Python. Files are loaded directly by the parent Stable Diffusion WebUI.
|
||||
|
|
@ -33,6 +38,15 @@ state.ext.*.js → Extension-specific handlers (ControlNet, ADetailer, et
|
|||
- All code lives under `window.state` namespace (`state.core`, `state.store`, `state.utils`, `state.extensions`)
|
||||
- Extension plugins register via `state.extensions[name] = { init: function() {} }`
|
||||
|
||||
**Gradio Compatibility Utilities** (`state.utils`):
|
||||
- `state.utils.gradio.detectVersion()` - Detects Gradio 3.x vs 4.x
|
||||
- `state.utils.getButtonClass()` - Returns appropriate button CSS classes
|
||||
- `state.utils.findDropdowns(container)` - Finds dropdowns with fallback selectors
|
||||
- `state.utils.findAccordions(container)` - Finds accordions with fallback selectors
|
||||
- `state.utils.getDropdownValue(select)` - Gets dropdown value (works with both Gradio versions)
|
||||
- `state.utils.getMultiSelectValues(select)` - Gets multi-select values
|
||||
- `state.utils.isAccordionOpen(accordion)` - Checks accordion state with multiple methods
|
||||
|
||||
### Backend (Python)
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ state.core = (function () {
|
|||
'hires_resize_x': 'hr_resize_x',
|
||||
'hires_resize_y': 'hr_resize_y',
|
||||
'hires_denoising_strength': 'denoising_strength',
|
||||
'hires_cfg_scale': 'hr_cfg',
|
||||
'refiner_switch': 'switch_at',
|
||||
'upscaler_2_visibility': 'extras_upscaler_2_visibility',
|
||||
'upscaler_scale_by_resize': 'extras_upscaling_resize',
|
||||
|
|
@ -225,13 +226,47 @@ state.core = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
// Get tab buttons with multiple fallback selectors for compatibility
|
||||
function getTabButtons() {
|
||||
var root = gradioApp();
|
||||
// Try multiple selectors for compatibility with different Gradio versions
|
||||
var tabs = root.querySelectorAll('#tabs > div:first-child button');
|
||||
if (!tabs.length) {
|
||||
tabs = root.querySelectorAll('#tabs .tab-nav button');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
tabs = root.querySelectorAll('#tabs > .tabs > .tab-nav button');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
// Gradio 4.x may have different structure
|
||||
tabs = root.querySelectorAll('#tabs button[role="tab"]');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
tabs = root.querySelectorAll('#tabs > div > button');
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
|
||||
// Get selected tab button with fallback
|
||||
function getSelectedTabButton() {
|
||||
var root = gradioApp();
|
||||
var selected = root.querySelector('#tabs .tab-nav button.selected');
|
||||
if (!selected) {
|
||||
selected = root.querySelector('#tabs button.selected');
|
||||
}
|
||||
if (!selected) {
|
||||
selected = root.querySelector('#tabs button[aria-selected="true"]');
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
function restoreTabs(config) {
|
||||
|
||||
if (! config.hasSetting('tabs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabs = gradioApp().querySelectorAll('#tabs > div:first-child button');
|
||||
const tabs = getTabButtons();
|
||||
const value = store.get('tab');
|
||||
|
||||
if (value) {
|
||||
|
|
@ -244,20 +279,23 @@ state.core = (function () {
|
|||
}
|
||||
// Use this when onUiTabChange is fixed
|
||||
// onUiTabChange(function () {
|
||||
// store.set('tab', gradioApp().querySelector('#tabs .tab-nav button.selected').textContent);
|
||||
// store.set('tab', getSelectedTabButton()?.textContent);
|
||||
// });
|
||||
bindTabClickEvents();
|
||||
}
|
||||
|
||||
function bindTabClickEvents() {
|
||||
Array.from(gradioApp().querySelectorAll('#tabs .tab-nav button')).forEach(tab => {
|
||||
Array.from(getTabButtons()).forEach(tab => {
|
||||
tab.removeEventListener('click', storeTab);
|
||||
tab.addEventListener('click', storeTab);
|
||||
});
|
||||
}
|
||||
|
||||
function storeTab() {
|
||||
store.set('tab', gradioApp().querySelector('#tabs .tab-nav button.selected').textContent);
|
||||
var selected = getSelectedTabButton();
|
||||
if (selected) {
|
||||
store.set('tab', selected.textContent);
|
||||
}
|
||||
bindTabClickEvents(); // dirty hack here...
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +326,12 @@ state.core = (function () {
|
|||
return;
|
||||
}
|
||||
|
||||
// Convert to array for easier handling
|
||||
elements = Array.from(elements);
|
||||
|
||||
// For sliders with both number input and range, prefer number input for storage
|
||||
let primaryElement = elements.find(el => el.type === 'number') || elements[0];
|
||||
|
||||
let forEach = function (action) {
|
||||
events.forEach(function(event) {
|
||||
elements.forEach(function (element) {
|
||||
|
|
@ -296,6 +340,7 @@ state.core = (function () {
|
|||
});
|
||||
};
|
||||
|
||||
// Attach event listeners to all elements
|
||||
forEach(function (event) {
|
||||
this.addEventListener(event, function () {
|
||||
let value = this.value;
|
||||
|
|
@ -306,17 +351,23 @@ state.core = (function () {
|
|||
});
|
||||
});
|
||||
|
||||
// Special handling for seed buttons
|
||||
if (id.indexOf('seed') > -1) {
|
||||
TABS.forEach(tab => {
|
||||
const seedInput = gradioApp().querySelector(`#${tab}_seed input`);
|
||||
['random_seed', 'reuse_seed'].forEach(id => {
|
||||
const btn = gradioApp().querySelector(`#${tab}_${id}`);
|
||||
const seedInput = gradioApp().querySelector(`#${tab}_seed input[type="number"]`);
|
||||
if (!seedInput) return;
|
||||
['random_seed', 'reuse_seed'].forEach(btnId => {
|
||||
const btn = gradioApp().querySelector(`#${tab}_${btnId}`);
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
setTimeout(() => {
|
||||
state.utils.triggerEvent(seedInput, 'change');
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let value = store.get(id);
|
||||
|
||||
|
|
@ -324,10 +375,68 @@ state.core = (function () {
|
|||
return;
|
||||
}
|
||||
|
||||
forEach(function (event) {
|
||||
state.utils.setValue(this, value, event);
|
||||
// Delay restoration to ensure UI and updateInput are ready
|
||||
// Use longer delay to ensure Gradio components are fully initialized
|
||||
setTimeout(function() {
|
||||
// Check if updateInput is available (it should be by now)
|
||||
if (typeof updateInput !== 'function') {
|
||||
state.logging.warn('updateInput not available yet, retrying...');
|
||||
setTimeout(function() {
|
||||
restoreInputValue(id, elements, value);
|
||||
}, 500);
|
||||
} else {
|
||||
restoreInputValue(id, elements, value);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function restoreInputValue(id, elements, value) {
|
||||
state.logging.log(`Attempting to restore ${id} with value: ${value}`);
|
||||
|
||||
// Check element types
|
||||
let numberInput = elements.find(el => el.type === 'number');
|
||||
let rangeInput = elements.find(el => el.type === 'range');
|
||||
let textareaInput = elements.find(el => el.tagName === 'TEXTAREA');
|
||||
let textInput = elements.find(el => el.type === 'text');
|
||||
|
||||
state.logging.log(`Found elements for ${id}: number=${!!numberInput}, range=${!!rangeInput}, textarea=${!!textareaInput}, text=${!!textInput}`);
|
||||
|
||||
// For sliders (have both number and range), use special handler
|
||||
if (numberInput && rangeInput) {
|
||||
// Get the container element
|
||||
let container = numberInput.closest('.gradio-slider, [class*="slider"]') ||
|
||||
numberInput.parentElement.parentElement;
|
||||
state.logging.log(`Updating slider container for ${id}`, container);
|
||||
state.utils.updateGradioSlider(container, value);
|
||||
|
||||
// Verify the value was set
|
||||
setTimeout(function() {
|
||||
state.logging.log(`After restore - ${id} number value: ${numberInput.value}, range value: ${rangeInput.value}`);
|
||||
}, 100);
|
||||
} else if (numberInput) {
|
||||
// Just number input
|
||||
state.utils.setValue(numberInput, value, 'input');
|
||||
state.logging.log(`Restored number ${id}: ${value}`);
|
||||
} else if (textareaInput) {
|
||||
// For textareas (prompts)
|
||||
state.utils.setValue(textareaInput, value, 'input');
|
||||
state.logging.log(`Restored textarea ${id}: ${value}`);
|
||||
} else if (textInput) {
|
||||
// For text inputs
|
||||
state.utils.setValue(textInput, value, 'input');
|
||||
state.logging.log(`Restored text ${id}: ${value}`);
|
||||
} else if (rangeInput) {
|
||||
// Fallback to range input
|
||||
state.utils.setValue(rangeInput, value, 'input');
|
||||
state.logging.log(`Restored range ${id}: ${value}`);
|
||||
} else {
|
||||
// For any other elements, update all
|
||||
state.logging.log(`Restoring ${id} to all ${elements.length} elements`);
|
||||
elements.forEach(function (element) {
|
||||
state.utils.setValue(element, value, 'input');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSavedSelects(id, duplicateIds) {
|
||||
if (duplicateIds) {
|
||||
|
|
@ -353,15 +462,29 @@ state.core = (function () {
|
|||
|
||||
let selector = duplicateIds ? `[id="${id}"]` : `#${id}`;
|
||||
|
||||
elements = gradioApp().querySelectorAll(`.input-accordion${selector}>.label-wrap`);
|
||||
// Try multiple selector patterns for compatibility
|
||||
var elements = gradioApp().querySelectorAll(`.input-accordion${selector}>.label-wrap`);
|
||||
if (!elements.length) {
|
||||
elements = gradioApp().querySelectorAll(`.input-accordion${selector} .label-wrap`);
|
||||
}
|
||||
if (!elements.length) {
|
||||
elements = gradioApp().querySelectorAll(`${selector} > .label-wrap`);
|
||||
}
|
||||
if (!elements.length) {
|
||||
elements = gradioApp().querySelectorAll(`${selector}.input-accordion > .label-wrap`);
|
||||
}
|
||||
|
||||
elements.forEach(function (element) {
|
||||
if (store.get(id) === 'true') {
|
||||
state.utils.clickToggleMenu(element);
|
||||
}
|
||||
element.addEventListener('click', function () {
|
||||
let classList = Array.from(this.parentNode.classList);
|
||||
store.set(id, classList.indexOf('input-accordion-open') > -1);
|
||||
var parent = this.parentNode;
|
||||
// Check for open state using multiple methods for compatibility
|
||||
var isOpen = parent.classList.contains('input-accordion-open') ||
|
||||
this.classList.contains('open') ||
|
||||
state.utils.isAccordionOpen(parent);
|
||||
store.set(id, isOpen);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,17 @@ function ControlNetTabContext(tabName, container) {
|
|||
this.tabElements = [];
|
||||
this.cnTabs = [];
|
||||
|
||||
// Try multiple selectors for compatibility with different Gradio/Forge versions
|
||||
let tabs = this.container.querySelectorAll(':scope > div > div > .tabs > .tabitem');
|
||||
if (!tabs.length) {
|
||||
tabs = this.container.querySelectorAll('.tabitem');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
tabs = this.container.querySelectorAll('[id*="tabitem"]');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
tabs = this.container.querySelectorAll('.tabs > div[role="tabpanel"]');
|
||||
}
|
||||
|
||||
if (tabs.length) {
|
||||
tabs.forEach((tabContainer, i) => {
|
||||
|
|
@ -21,10 +31,10 @@ function ControlNetTabContext(tabName, container) {
|
|||
});
|
||||
});
|
||||
} else {
|
||||
this.cnTabs.push[{
|
||||
this.cnTabs.push({
|
||||
container: container,
|
||||
store: new state.Store(`ext-control-net-${this.tabName}-${i}`)
|
||||
}];
|
||||
store: new state.Store(`ext-control-net-${this.tabName}-0`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +48,11 @@ state.extensions['control-net'] = (function () {
|
|||
|
||||
contexts.forEach(context => {
|
||||
|
||||
const elements = context.container.querySelectorAll(`:scope > .label-wrap`)
|
||||
// Try multiple selectors for compatibility
|
||||
let elements = context.container.querySelectorAll(`:scope > .label-wrap`);
|
||||
if (!elements.length) {
|
||||
elements = context.container.querySelectorAll('.label-wrap');
|
||||
}
|
||||
|
||||
elements.forEach(element => {
|
||||
if (context.store.get(id) === 'true') {
|
||||
|
|
@ -46,8 +60,11 @@ state.extensions['control-net'] = (function () {
|
|||
load();
|
||||
}
|
||||
element.addEventListener('click', function () {
|
||||
let classList = Array.from(this.classList);
|
||||
context.store.set(id, classList.indexOf('open') > -1);
|
||||
// Check for open state using multiple methods for compatibility
|
||||
let isOpen = this.classList.contains('open') ||
|
||||
this.parentNode.classList.contains('open') ||
|
||||
state.utils.isAccordionOpen(this.parentNode);
|
||||
context.store.set(id, isOpen);
|
||||
load();
|
||||
});
|
||||
});
|
||||
|
|
@ -56,7 +73,18 @@ state.extensions['control-net'] = (function () {
|
|||
|
||||
function bindTabEvents() {
|
||||
contexts.forEach(context => {
|
||||
const tabs = context.container.querySelectorAll(':scope > div > div > .tabs > div > button');
|
||||
// Try multiple selectors for compatibility
|
||||
let tabs = context.container.querySelectorAll(':scope > div > div > .tabs > div > button');
|
||||
if (!tabs.length) {
|
||||
tabs = context.container.querySelectorAll('.tabs .tab-nav button');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
tabs = context.container.querySelectorAll('.tabs > div > button');
|
||||
}
|
||||
if (!tabs.length) {
|
||||
tabs = context.container.querySelectorAll('button[role="tab"]');
|
||||
}
|
||||
|
||||
function onTabClick() {
|
||||
context.store.set('tab', this.textContent);
|
||||
bindTabEvents();
|
||||
|
|
@ -92,12 +120,84 @@ state.extensions['control-net'] = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
// Helper to find checkbox label text
|
||||
function getCheckboxLabel(checkbox) {
|
||||
// Try nextElementSibling (common pattern)
|
||||
if (checkbox.nextElementSibling && checkbox.nextElementSibling.textContent) {
|
||||
return checkbox.nextElementSibling.textContent;
|
||||
}
|
||||
|
||||
// Try parent label
|
||||
let parentLabel = checkbox.closest('label');
|
||||
if (parentLabel) {
|
||||
// Get text excluding the checkbox itself
|
||||
let clone = parentLabel.cloneNode(true);
|
||||
let input = clone.querySelector('input');
|
||||
if (input) input.remove();
|
||||
if (clone.textContent && clone.textContent.trim()) {
|
||||
return clone.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Try aria-label or title
|
||||
if (checkbox.getAttribute('aria-label')) return checkbox.getAttribute('aria-label');
|
||||
if (checkbox.title) return checkbox.title;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to find select/dropdown label text
|
||||
function getSelectLabel(select) {
|
||||
// Try label inside the select container
|
||||
let label = select.querySelector('label');
|
||||
if (label) {
|
||||
if (label.firstChild && label.firstChild.textContent) {
|
||||
return label.firstChild.textContent;
|
||||
}
|
||||
if (label.textContent) return label.textContent;
|
||||
}
|
||||
|
||||
// Try span with label class
|
||||
let span = select.querySelector('span[data-testid="block-label"], span[class*="label"]');
|
||||
if (span && span.textContent) return span.textContent;
|
||||
|
||||
// Try previous sibling
|
||||
if (select.previousElementSibling) {
|
||||
let prevLabel = select.previousElementSibling.querySelector('label, span');
|
||||
if (prevLabel && prevLabel.textContent) return prevLabel.textContent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to find textarea label text
|
||||
function getTextareaLabel(textarea) {
|
||||
// Try previousElementSibling
|
||||
if (textarea.previousElementSibling && textarea.previousElementSibling.textContent) {
|
||||
return textarea.previousElementSibling.textContent;
|
||||
}
|
||||
|
||||
// Try parent container for label
|
||||
let parent = textarea.closest('.gradio-textbox, [class*="textbox"]');
|
||||
if (parent) {
|
||||
let label = parent.querySelector('label, span[data-testid="block-label"]');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
}
|
||||
|
||||
// Try aria-label or placeholder
|
||||
if (textarea.getAttribute('aria-label')) return textarea.getAttribute('aria-label');
|
||||
if (textarea.placeholder) return textarea.placeholder;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleCheckboxes() {
|
||||
handleContext((container, store) => {
|
||||
let checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(function (checkbox) {
|
||||
let label = checkbox.nextElementSibling;
|
||||
let id = state.utils.txtToId(label.textContent);
|
||||
checkboxes.forEach(function (checkbox, idx) {
|
||||
let labelText = getCheckboxLabel(checkbox);
|
||||
// Use index-based fallback if no label found
|
||||
let id = labelText ? state.utils.txtToId(labelText) : `checkbox-${idx}`;
|
||||
let value = store.get(id);
|
||||
if (value) {
|
||||
state.utils.setValue(checkbox, value, 'change');
|
||||
|
|
@ -111,8 +211,12 @@ state.extensions['control-net'] = (function () {
|
|||
|
||||
function handleSelects() {
|
||||
handleContext((container, store) => {
|
||||
container.querySelectorAll('.gradio-dropdown').forEach(select => {
|
||||
let id = state.utils.txtToId(select.querySelector('label').firstChild.textContent);
|
||||
// Use compatibility helper to find dropdowns
|
||||
let dropdowns = state.utils.findDropdowns(container);
|
||||
dropdowns.forEach(function (select, idx) {
|
||||
let labelText = getSelectLabel(select);
|
||||
// Use index-based fallback if no label found
|
||||
let id = labelText ? state.utils.txtToId(labelText) : `select-${idx}`;
|
||||
let value = store.get(id);
|
||||
state.utils.handleSelect(select, id, store);
|
||||
if (id === 'preprocessor' && value && value.toLowerCase() !== 'none') {
|
||||
|
|
@ -122,12 +226,64 @@ state.extensions['control-net'] = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
// Helper to find slider label text with multiple fallback methods
|
||||
function getSliderLabel(slider) {
|
||||
// Try previousElementSibling first (old structure)
|
||||
if (slider.previousElementSibling) {
|
||||
let label = slider.previousElementSibling.querySelector('label span');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
|
||||
// Try just the label
|
||||
label = slider.previousElementSibling.querySelector('label');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
|
||||
// Try span directly
|
||||
label = slider.previousElementSibling.querySelector('span');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
}
|
||||
|
||||
// Try parent container for label (Forge/Gradio 4.x structure)
|
||||
let parent = slider.closest('.gradio-slider, .slider, [class*="slider"]');
|
||||
if (parent) {
|
||||
let label = parent.querySelector('label span, label, span[data-testid="block-label"]');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
}
|
||||
|
||||
// Try looking for label in parent's previous sibling
|
||||
if (slider.parentElement && slider.parentElement.previousElementSibling) {
|
||||
let label = slider.parentElement.previousElementSibling.querySelector('span, label');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to find fieldset/radio label text
|
||||
function getFieldsetLabel(fieldset) {
|
||||
// Try firstChild.nextElementSibling (old structure)
|
||||
if (fieldset.firstChild && fieldset.firstChild.nextElementSibling) {
|
||||
let label = fieldset.firstChild.nextElementSibling;
|
||||
if (label && label.textContent) return label.textContent;
|
||||
}
|
||||
|
||||
// Try legend element
|
||||
let legend = fieldset.querySelector('legend');
|
||||
if (legend && legend.textContent) return legend.textContent;
|
||||
|
||||
// Try label or span
|
||||
let label = fieldset.querySelector('label, span[class*="label"]');
|
||||
if (label && label.textContent) return label.textContent;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleSliders() {
|
||||
handleContext((container, store) => {
|
||||
let sliders = container.querySelectorAll('input[type="range"]');
|
||||
sliders.forEach(function (slider) {
|
||||
let label = slider.previousElementSibling.querySelector('label span');
|
||||
let id = state.utils.txtToId(label.textContent);
|
||||
sliders.forEach(function (slider, idx) {
|
||||
let labelText = getSliderLabel(slider);
|
||||
// Use index-based fallback if no label found
|
||||
let id = labelText ? state.utils.txtToId(labelText) : `slider-${idx}`;
|
||||
let value = store.get(id);
|
||||
if (value) {
|
||||
state.utils.setValue(slider, value, 'change');
|
||||
|
|
@ -142,10 +298,11 @@ state.extensions['control-net'] = (function () {
|
|||
function handleRadioButtons() {
|
||||
handleContext((container, store) => {
|
||||
let fieldsets = container.querySelectorAll('fieldset');
|
||||
fieldsets.forEach(function (fieldset) {
|
||||
let label = fieldset.firstChild.nextElementSibling;
|
||||
fieldsets.forEach(function (fieldset, idx) {
|
||||
let radios = fieldset.querySelectorAll('input[type="radio"]');
|
||||
let id = state.utils.txtToId(label.textContent);
|
||||
let labelText = getFieldsetLabel(fieldset);
|
||||
// Use index-based fallback if no label found
|
||||
let id = labelText ? state.utils.txtToId(labelText) : `radio-${idx}`;
|
||||
let value = store.get(id);
|
||||
if (value) {
|
||||
radios.forEach(function (radio) {
|
||||
|
|
@ -164,9 +321,10 @@ state.extensions['control-net'] = (function () {
|
|||
function handleTextareas() {
|
||||
handleContext((container, store) => {
|
||||
let textareas = container.querySelectorAll('textarea');
|
||||
textareas.forEach(function (textarea) {
|
||||
let label = textarea.previousElementSibling;
|
||||
let id = state.utils.txtToId(label.textContent);
|
||||
textareas.forEach(function (textarea, idx) {
|
||||
let labelText = getTextareaLabel(textarea);
|
||||
// Use index-based fallback if no label found
|
||||
let id = labelText ? state.utils.txtToId(labelText) : `textarea-${idx}`;
|
||||
let value = store.get(id);
|
||||
if (value) {
|
||||
state.utils.setValue(textarea, value, 'change');
|
||||
|
|
@ -191,14 +349,32 @@ state.extensions['control-net'] = (function () {
|
|||
|
||||
function init() {
|
||||
|
||||
// Try multiple selectors for ControlNet container (Forge vs A1111 compatibility)
|
||||
let elements = gradioApp().querySelectorAll('#controlnet');
|
||||
if (!elements.length) {
|
||||
elements = gradioApp().querySelectorAll('[id*="controlnet"]');
|
||||
}
|
||||
if (!elements.length) {
|
||||
elements = gradioApp().querySelectorAll('#txt2img_controlnet, #img2img_controlnet');
|
||||
}
|
||||
// Forge built-in ControlNet uses different IDs
|
||||
if (!elements.length) {
|
||||
elements = gradioApp().querySelectorAll('[id*="forge_controlnet"], [id*="sd_forge_controlnet"]');
|
||||
}
|
||||
|
||||
if (! elements.length) {
|
||||
if (!elements.length) {
|
||||
state.logging.log('ControlNet extension not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle both single container and separate txt2img/img2img containers
|
||||
if (elements.length >= 2) {
|
||||
contexts[0] = new ControlNetTabContext('txt2img', elements[0]);
|
||||
contexts[1] = new ControlNetTabContext('img2img', elements[1]);
|
||||
} else if (elements.length === 1) {
|
||||
// Single container mode
|
||||
contexts[0] = new ControlNetTabContext('main', elements[0]);
|
||||
}
|
||||
|
||||
handleToggle();
|
||||
load();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ state.extensions['multidiffusion'] = (function () {
|
|||
}
|
||||
|
||||
function handleSelects(container, name) {
|
||||
let selects = container.querySelectorAll('.gradio-dropdown')
|
||||
// Use compatibility helper to find dropdowns
|
||||
let selects = state.utils.findDropdowns(container);
|
||||
selects.forEach(function (select, idx) {
|
||||
state.utils.handleSelect(select, `md-${name}-select-${idx}`, store);
|
||||
});
|
||||
|
|
@ -76,17 +77,24 @@ state.extensions['multidiffusion'] = (function () {
|
|||
}
|
||||
|
||||
function handleDropdowns(container, name) {
|
||||
let dropdowns = container.querySelectorAll('.gradio-accordion .label-wrap');
|
||||
dropdowns.forEach(function (dropdown, idx) {
|
||||
// Use compatibility helper to find accordions
|
||||
let accordions = state.utils.findAccordions(container);
|
||||
accordions.forEach(function (accordion, idx) {
|
||||
let labelWrap = accordion.querySelector('.label-wrap');
|
||||
if (!labelWrap) return;
|
||||
|
||||
let id = `md-${name}-dropdown-${idx}`;
|
||||
let value = store.get(id);
|
||||
|
||||
if (value && value === 'true') {
|
||||
state.utils.triggerEvent(dropdown, 'click');
|
||||
state.utils.triggerEvent(labelWrap, 'click');
|
||||
}
|
||||
dropdown.addEventListener('click', function () {
|
||||
let span = this.querySelector('.transition, .icon');
|
||||
store.set(id, span.style.transform !== 'rotate(90deg)');
|
||||
labelWrap.addEventListener('click', function () {
|
||||
// Use multiple methods to check open state for compatibility
|
||||
let isOpen = this.classList.contains('open') ||
|
||||
this.parentNode.classList.contains('open') ||
|
||||
state.utils.isAccordionOpen(this.parentNode);
|
||||
store.set(id, isOpen);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -106,20 +114,47 @@ state.extensions['multidiffusion'] = (function () {
|
|||
|
||||
function init() {
|
||||
|
||||
// Try to find Tiled Diffusion/VAE containers by text content
|
||||
let spanTags = gradioApp().getElementsByTagName("span");
|
||||
for (var i = 0; i < spanTags.length; i++) {
|
||||
if (spanTags[i].textContent == 'Tiled Diffusion') {
|
||||
containers.push({container: spanTags[i].parentElement.parentElement,name: 'diffusion'});
|
||||
let text = spanTags[i].textContent.trim();
|
||||
if (text === 'Tiled Diffusion' || text === 'Tiled Diffusion (Multidiffusion)') {
|
||||
let parent = spanTags[i].parentElement;
|
||||
// Navigate up to find the accordion container
|
||||
while (parent && !parent.classList.contains('gradio-accordion') && !parent.classList.contains('accordion') && !parent.id) {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
if (parent) {
|
||||
containers.push({container: parent, name: 'diffusion'});
|
||||
}
|
||||
}
|
||||
if (text === 'Tiled VAE') {
|
||||
let parent = spanTags[i].parentElement;
|
||||
while (parent && !parent.classList.contains('gradio-accordion') && !parent.classList.contains('accordion') && !parent.id) {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
if (parent) {
|
||||
containers.push({container: parent, name: 'vae'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try by ID for Forge compatibility
|
||||
if (!containers.length) {
|
||||
let tiledDiff = gradioApp().querySelector('[id*="tiled_diffusion"], [id*="multidiffusion"]');
|
||||
if (tiledDiff) {
|
||||
containers.push({container: tiledDiff, name: 'diffusion'});
|
||||
}
|
||||
let tiledVae = gradioApp().querySelector('[id*="tiled_vae"]');
|
||||
if (tiledVae) {
|
||||
containers.push({container: tiledVae, name: 'vae'});
|
||||
}
|
||||
if (spanTags[i].textContent == 'Tiled VAE') {
|
||||
containers.push({container: spanTags[i].parentElement.parentElement,name: 'vae'});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
store = new state.Store('ext-multidiffusion');
|
||||
|
||||
if (! containers.length) {
|
||||
if (!containers.length) {
|
||||
state.logging.log('Multidiffusion/Tiled VAE extension not found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,45 @@ state.logging = {
|
|||
|
||||
name: 'state',
|
||||
|
||||
log: function (message) {
|
||||
// Set to true to enable debug logging
|
||||
DEBUG: false,
|
||||
|
||||
log: function (message, data) {
|
||||
if (!this.DEBUG) return;
|
||||
if (data !== undefined) {
|
||||
console.log(`[${this.name}]: `, message, data);
|
||||
} else {
|
||||
console.log(`[${this.name}]: `, message);
|
||||
}
|
||||
},
|
||||
|
||||
error: function (message) {
|
||||
error: function (message, data) {
|
||||
// Errors are always shown
|
||||
if (data !== undefined) {
|
||||
console.error(`[${this.name}]: `, message, data);
|
||||
} else {
|
||||
console.error(`[${this.name}]: `, message);
|
||||
}
|
||||
},
|
||||
|
||||
warn: function (message) {
|
||||
warn: function (message, data) {
|
||||
if (!this.DEBUG) return;
|
||||
if (data !== undefined) {
|
||||
console.warn(`[${this.name}]: `, message, data);
|
||||
} else {
|
||||
console.warn(`[${this.name}]: `, message);
|
||||
}
|
||||
},
|
||||
|
||||
// Call this in browser console to enable debugging: state.logging.enable()
|
||||
enable: function() {
|
||||
this.DEBUG = true;
|
||||
console.log(`[${this.name}]: Debug logging enabled`);
|
||||
},
|
||||
|
||||
// Call this in browser console to disable debugging: state.logging.disable()
|
||||
disable: function() {
|
||||
this.DEBUG = false;
|
||||
console.log(`[${this.name}]: Debug logging disabled`);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,10 +46,107 @@ state.utils = {
|
|||
element.checked = false;
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
case 'range':
|
||||
// For sliders and number inputs, use Forge pattern
|
||||
element.value = value;
|
||||
if (typeof updateInput === 'function') {
|
||||
updateInput(element);
|
||||
}
|
||||
break;
|
||||
case 'textarea':
|
||||
// Textareas (prompts) - use Forge pattern
|
||||
element.value = value;
|
||||
if (typeof updateInput === 'function') {
|
||||
updateInput(element);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
element.value = value;
|
||||
// For text inputs and other types, use Forge pattern if available
|
||||
if (typeof updateInput === 'function' && (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT')) {
|
||||
updateInput(element);
|
||||
} else {
|
||||
this.triggerEvent(element, event);
|
||||
}
|
||||
}
|
||||
},
|
||||
// Update input using Gradio's expected event format (works with Forge and new Gradio)
|
||||
updateGradioInput: function updateGradioInput(target) {
|
||||
if (!target) return;
|
||||
|
||||
// Use the global updateInput if available (Forge/A1111)
|
||||
if (typeof updateInput === 'function') {
|
||||
updateInput(target);
|
||||
}
|
||||
|
||||
// Also dispatch events manually to ensure Svelte/Gradio components update
|
||||
// Input event with bubbles
|
||||
let inputEvent = new Event("input", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(inputEvent, "target", { value: target });
|
||||
target.dispatchEvent(inputEvent);
|
||||
|
||||
// Change event
|
||||
let changeEvent = new Event("change", { bubbles: true, cancelable: true });
|
||||
target.dispatchEvent(changeEvent);
|
||||
|
||||
// For Svelte components, also try InputEvent
|
||||
try {
|
||||
let inputEvent2 = new InputEvent("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
inputType: "insertText",
|
||||
data: target.value
|
||||
});
|
||||
target.dispatchEvent(inputEvent2);
|
||||
} catch (e) {
|
||||
// InputEvent might not be supported in all browsers
|
||||
}
|
||||
},
|
||||
|
||||
// Set value using native setter to bypass framework reactivity issues
|
||||
setNativeValue: function setNativeValue(element, value) {
|
||||
// Get the native value setter
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(element.__proto__, 'value')?.set ||
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set ||
|
||||
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
||||
|
||||
if (valueSetter) {
|
||||
valueSetter.call(element, value);
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
},
|
||||
|
||||
// Update a Gradio slider component - matches exact Forge pattern
|
||||
updateGradioSlider: function updateGradioSlider(container, value) {
|
||||
if (!container) return;
|
||||
|
||||
let numberInput = container.querySelector('input[type=number]');
|
||||
let rangeInput = container.querySelector('input[type=range]');
|
||||
|
||||
// Use exact Forge pattern: set .value then call updateInput()
|
||||
if (numberInput) {
|
||||
numberInput.value = value;
|
||||
if (typeof updateInput === 'function') {
|
||||
updateInput(numberInput);
|
||||
}
|
||||
}
|
||||
|
||||
// Also update range for visual sync
|
||||
if (rangeInput) {
|
||||
rangeInput.value = value;
|
||||
if (typeof updateInput === 'function') {
|
||||
updateInput(rangeInput);
|
||||
}
|
||||
|
||||
// Update visual slider fill
|
||||
let min = parseFloat(rangeInput.min) || 0;
|
||||
let max = parseFloat(rangeInput.max) || 100;
|
||||
let val = parseFloat(value) || 0;
|
||||
let percentage = ((val - min) / (max - min)) * 100;
|
||||
rangeInput.style.backgroundSize = percentage + '% 100%';
|
||||
}
|
||||
},
|
||||
onContentChange: function onContentChange(targetNode, func) {
|
||||
const observer = new MutationObserver((mutationsList, observer) => {
|
||||
|
|
@ -91,17 +188,22 @@ state.utils = {
|
|||
|
||||
setTimeout(() => {
|
||||
state.utils.onContentChange(select, function (el) {
|
||||
let selected = el.querySelector('span.single-select');
|
||||
if (selected) {
|
||||
store.set(id, selected.textContent);
|
||||
} else {
|
||||
// new gradio version...
|
||||
let input = select.querySelector('input');
|
||||
if (input) {
|
||||
store.set(id, input.value);
|
||||
}
|
||||
// Use compatibility helper to get dropdown value
|
||||
var value = state.utils.getDropdownValue(el);
|
||||
if (value) {
|
||||
store.set(id, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Also listen to input events directly for Gradio 4.x
|
||||
let input = select.querySelector('input');
|
||||
if (input) {
|
||||
input.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
store.set(id, this.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 150);
|
||||
} catch (error) {
|
||||
console.error('[state]: Error:', error);
|
||||
|
|
@ -144,7 +246,8 @@ state.utils = {
|
|||
}
|
||||
}
|
||||
state.utils.onContentChange(select, function (el) {
|
||||
const selected = Array.from(el.querySelectorAll('.token > span')).map(item => item.textContent);
|
||||
// Use compatibility helper to get multi-select values
|
||||
const selected = state.utils.getMultiSelectValues(el);
|
||||
store.set(id, selected);
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -225,7 +328,167 @@ state.utils.html = {
|
|||
const btn = document.createElement('button');
|
||||
btn.innerHTML = text;
|
||||
btn.onclick = onclick || function () {};
|
||||
btn.className = 'gr-button gr-button-lg gr-button-primary';
|
||||
// Support both old Gradio 3.x and new Gradio 4.x button classes
|
||||
btn.className = state.utils.getButtonClass();
|
||||
return btn;
|
||||
},
|
||||
// Get the appropriate button class based on Gradio version
|
||||
getButtonClass: function() {
|
||||
return state.utils.getButtonClass();
|
||||
}
|
||||
};
|
||||
|
||||
// Gradio version detection and compatibility helpers
|
||||
state.utils.gradio = {
|
||||
_version: null,
|
||||
_detected: false,
|
||||
|
||||
// Detect Gradio version based on available DOM elements/classes
|
||||
detectVersion: function() {
|
||||
if (this._detected) return this._version;
|
||||
|
||||
var root = gradioApp();
|
||||
|
||||
// Check for Gradio 4.x indicators
|
||||
if (root.querySelector('.gradio-container-4-')) {
|
||||
this._version = 4;
|
||||
} else if (root.querySelector('[class*="gradio-container-4"]')) {
|
||||
this._version = 4;
|
||||
} else if (root.querySelector('.svelte-')) {
|
||||
// Gradio 4.x uses svelte classes
|
||||
this._version = 4;
|
||||
} else {
|
||||
// Default to 3.x for older versions
|
||||
this._version = 3;
|
||||
}
|
||||
|
||||
this._detected = true;
|
||||
state.logging.log('Detected Gradio version: ' + this._version + '.x');
|
||||
return this._version;
|
||||
},
|
||||
|
||||
isVersion4: function() {
|
||||
return this.detectVersion() >= 4;
|
||||
}
|
||||
};
|
||||
|
||||
// Get button class with fallback support
|
||||
state.utils.getButtonClass = function() {
|
||||
var root = gradioApp();
|
||||
// Try to find an existing button and copy its class
|
||||
var existingBtn = root.querySelector('#quicksettings button');
|
||||
if (existingBtn && existingBtn.className) {
|
||||
return existingBtn.className;
|
||||
}
|
||||
// Fallback class names - try both old and new
|
||||
if (state.utils.gradio.isVersion4()) {
|
||||
return 'lg primary gradio-button svelte-cmf5ev';
|
||||
}
|
||||
return 'gr-button gr-button-lg gr-button-primary';
|
||||
};
|
||||
|
||||
// Find dropdown elements with fallback selectors
|
||||
state.utils.findDropdowns = function(container) {
|
||||
container = container || gradioApp();
|
||||
// Try multiple selectors for compatibility
|
||||
var dropdowns = container.querySelectorAll('.gradio-dropdown');
|
||||
if (!dropdowns.length) {
|
||||
dropdowns = container.querySelectorAll('[data-testid="dropdown"]');
|
||||
}
|
||||
if (!dropdowns.length) {
|
||||
dropdowns = container.querySelectorAll('.dropdown');
|
||||
}
|
||||
return dropdowns;
|
||||
};
|
||||
|
||||
// Find accordion elements with fallback selectors
|
||||
state.utils.findAccordions = function(container) {
|
||||
container = container || gradioApp();
|
||||
var accordions = container.querySelectorAll('.gradio-accordion');
|
||||
if (!accordions.length) {
|
||||
accordions = container.querySelectorAll('.accordion');
|
||||
}
|
||||
if (!accordions.length) {
|
||||
accordions = container.querySelectorAll('[data-testid="accordion"]');
|
||||
}
|
||||
return accordions;
|
||||
};
|
||||
|
||||
// Get selected value from dropdown with version compatibility
|
||||
state.utils.getDropdownValue = function(select) {
|
||||
if (!select) return null;
|
||||
|
||||
// Try new Gradio 4.x input method first
|
||||
var input = select.querySelector('input');
|
||||
if (input && input.value) {
|
||||
return input.value;
|
||||
}
|
||||
|
||||
// Try old Gradio 3.x span method
|
||||
var selected = select.querySelector('span.single-select');
|
||||
if (selected) {
|
||||
return selected.textContent;
|
||||
}
|
||||
|
||||
// Try other common patterns
|
||||
var selectedOption = select.querySelector('.selected');
|
||||
if (selectedOption) {
|
||||
return selectedOption.textContent;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Get multi-select values with version compatibility
|
||||
state.utils.getMultiSelectValues = function(select) {
|
||||
if (!select) return [];
|
||||
|
||||
// Try token pattern (common in both versions)
|
||||
var tokens = select.querySelectorAll('.token > span, .token span:first-child');
|
||||
if (tokens.length) {
|
||||
return Array.from(tokens).map(item => item.textContent);
|
||||
}
|
||||
|
||||
// Try secondary-wrap pattern (Gradio 4.x)
|
||||
var secondary = select.querySelectorAll('.secondary-wrap .token');
|
||||
if (secondary.length) {
|
||||
return Array.from(secondary).map(item => item.textContent.trim());
|
||||
}
|
||||
|
||||
// Try pill/tag pattern
|
||||
var pills = select.querySelectorAll('.pill, .tag, [data-value]');
|
||||
if (pills.length) {
|
||||
return Array.from(pills).map(item => item.textContent || item.dataset.value);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// Check if accordion is open with version compatibility
|
||||
state.utils.isAccordionOpen = function(accordion) {
|
||||
if (!accordion) return false;
|
||||
|
||||
var labelWrap = accordion.querySelector('.label-wrap');
|
||||
if (labelWrap) {
|
||||
// Check for 'open' class (Forge/A1111 style)
|
||||
if (labelWrap.classList.contains('open')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for input-accordion-open class
|
||||
if (accordion.classList.contains('input-accordion-open')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check icon rotation (older pattern)
|
||||
var icon = accordion.querySelector('.transition, .icon');
|
||||
if (icon) {
|
||||
var transform = icon.style.transform || window.getComputedStyle(icon).transform;
|
||||
if (transform && transform.indexOf('90') === -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
@ -1,18 +1,29 @@
|
|||
"""
|
||||
State extension settings for Stable Diffusion WebUI.
|
||||
Compatible with both AUTOMATIC1111 and Forge WebUI.
|
||||
"""
|
||||
|
||||
import gradio as gr
|
||||
import modules.shared as shared
|
||||
from modules import scripts
|
||||
|
||||
|
||||
def on_ui_settings():
|
||||
"""
|
||||
Register the State extension settings in the WebUI settings page.
|
||||
Uses CheckboxGroup for compatibility with both Gradio 3.x and 4.x.
|
||||
"""
|
||||
|
||||
section = ("state", "State")
|
||||
|
||||
# Main elements (tabs)
|
||||
shared.opts.add_option("state", shared.OptionInfo([], "Saved main elements", gr.CheckboxGroup, lambda: {
|
||||
"choices": [
|
||||
"tabs"
|
||||
]
|
||||
}, section=section))
|
||||
|
||||
# txt2img elements - includes Forge-specific options
|
||||
shared.opts.add_option("state_txt2img", shared.OptionInfo([], "Saved elements from txt2img", gr.CheckboxGroup, lambda: {
|
||||
"choices": [
|
||||
"prompt",
|
||||
|
|
@ -34,6 +45,7 @@ def on_ui_settings():
|
|||
"hires_resize_x",
|
||||
"hires_resize_y",
|
||||
"hires_denoising_strength",
|
||||
"hires_cfg_scale",
|
||||
"refiner",
|
||||
"refiner_checkpoint",
|
||||
"refiner_switch",
|
||||
|
|
@ -50,6 +62,7 @@ def on_ui_settings():
|
|||
]
|
||||
}, section=section))
|
||||
|
||||
# img2img elements
|
||||
shared.opts.add_option("state_img2img", shared.OptionInfo([], "Saved elements from img2img", gr.CheckboxGroup, lambda: {
|
||||
"choices": [
|
||||
"prompt",
|
||||
|
|
@ -82,6 +95,7 @@ def on_ui_settings():
|
|||
]
|
||||
}, section=section))
|
||||
|
||||
# Extension support - includes Forge built-in extensions
|
||||
shared.opts.add_option("state_extensions", shared.OptionInfo([], "Saved elements from extensions", gr.CheckboxGroup, lambda: {
|
||||
"choices": [
|
||||
"control-net",
|
||||
|
|
@ -91,6 +105,7 @@ def on_ui_settings():
|
|||
]
|
||||
}, section=section))
|
||||
|
||||
# UI buttons configuration
|
||||
shared.opts.add_option("state_ui", shared.OptionInfo([
|
||||
"Reset Button",
|
||||
"Import Button",
|
||||
|
|
@ -103,4 +118,8 @@ def on_ui_settings():
|
|||
],
|
||||
}, section=section))
|
||||
|
||||
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}")
|
||||
|
|
|
|||
Loading…
Reference in New Issue