dikdimon's picture
Upload sd-webui-tabs-extension using SD-Hub
2b0bc5f verified
// ECMAScript 2020+
// Разбор расширений и подготовка для табов
class TabsExtensionParser {
/** Клонируем label с чекбоксом и возвращаем именно label (или null)
* @param {boolean} enabled
* @returns {HTMLLabelElement|null}
*/
static #cloneCheckbox(enabled) {
const base = document.getElementById("TABSEX_CHECKBOX");
if (!base) return null;
const label = base.querySelector('label')?.cloneNode(true);
if (!label) return null;
label.style.margin = "1em 0em";
label.checkbox = label.querySelector("input");
if (label.checkbox) label.checkbox.checked = !!enabled;
return label;
}
/**
* Убираем версию из имени (если есть). Пустые строки -> null.
* @param {string} name
* @returns {string|null}
*/
static #sanitizeExtensionName(name) {
if (typeof name !== 'string' || name.trim().length === 0) return null;
const version_pattern = /([Vv](?:er)?[\.\s]*\d)/;
return name.split(version_pattern)[0].trim();
}
/**
* Разбор одного узла из контейнера расширений
* @param {HTMLDivElement} node
* @param {string[]} keep
* @returns {[string|null, HTMLDivElement|null]}
*/
static #parseObject(node, keep) {
if (!node) return [null, null];
// Спец-случай: форма со списком скриптов
if (node.classList.contains("form")) {
const scripts = node.querySelector(".gradio-dropdown");
if (!scripts) return [null, null];
const script_block = document.createElement("div");
script_block.style.display = 'none';
scripts.style.margin = '10px 0px';
script_block.appendChild(scripts);
script_block.setAttribute("ext-label", "Scripts");
// прячем исходный form-узел, чтобы не было дубля; решение о дубликате принимает parse()
node.style.display = "none";
return ["Scripts", script_block];
}
const styler = node.querySelector(".styler");
if (!styler) return [null, null];
const accordion = node.querySelector(".gradio-accordion");
if (!accordion) return [null, null];
const isInput = accordion.classList.contains("input-accordion");
const displayName = accordion.querySelector(".label-wrap>span")?.textContent;
if (!displayName) return [null, null];
const extensionName = this.#sanitizeExtensionName(displayName);
if (!extensionName || keep.includes(extensionName)) return [null, null];
const contents = [...accordion.children].filter(
(div) => !div.classList.contains("hide")
&& !div.classList.contains("label-wrap")
&& div.children.length > 0
);
if (contents.length === 0) return [null, null];
const content = contents[0];
if (isInput) {
const checkbox = accordion.querySelector(".input-accordion-checkbox");
const dummy = this.#cloneCheckbox(checkbox?.checked ?? false);
// Ставим обработчики только если всё есть
if (dummy && dummy.checkbox && checkbox) {
dummy.checkbox.onchange = () => {
if (checkbox.checked !== dummy.checkbox.checked) checkbox.click();
};
checkbox.onchange = () => {
if (checkbox.checked !== dummy.checkbox.checked) dummy.checkbox.click();
};
content.insertBefore(dummy, content.firstElementChild);
}
}
// Прячем исходный блок с аккордеоном
node.style.display = "none";
// Переносим якорь на новый content (и переименовываем старый id безопасно)
if (accordion.id && !accordion.id.startsWith("component-")) {
content.id = accordion.id;
accordion.id = `moved-${accordion.id}`;
if (isInput) {
// Предотвращаем ошибки в консоли в сетапе webui
content.visibleCheckbox = { checked: null };
content.onVisibleCheckboxChange = () => {};
}
}
content.setAttribute("ext-label", displayName);
return [extensionName, content];
}
/**
* Доп. блок "Extra Options"
* @param {'txt'|'img'} mode
* @returns {[string|null, HTMLDivElement|null]}
*/
static #extra_options(mode) {
const extra_options = document.getElementById(`extra_options_${mode}2img`);
if (!extra_options || !extra_options.classList.contains("gradio-accordion"))
return [null, null];
const styler = extra_options.parentElement;
if (styler) styler.style.display = "none";
const contents = [...extra_options.children].filter(
(div) => !div.classList.contains("hide")
&& !div.classList.contains("label-wrap")
&& div.children.length > 0
);
if (contents.length === 0) return [null, null];
const content = contents[0];
content.setAttribute("ext-label", "Extra Options");
return ["Extra Options", content];
}
/**
* Разбор всех расширений
* @param {'txt'|'img'} mode
* @param {string[]} keep
* @returns {Object.<string, HTMLDivElement>}
*/
static parse(mode, keep) {
const validExtensions = {};
const container = document.getElementById(`${mode}2img_script_container`);
const styler = container?.querySelector(".styler");
if (!container || !styler) return validExtensions;
const children = Array.from(styler.children);
let foundScripts = false; // флаг "Scripts уже встретился" в ЭТОМ контейнере
// Важно: НИЧЕГО больше не «скидываем» в Scripts — разбираем каждый узел отдельно.
for (const node of children) {
// не допускаем второй Scripts в пределах текущего контейнера
if (foundScripts && node.classList.contains("form")) {
node.style.display = "none";
continue;
}
const [name, content] = this.#parseObject(node, keep);
if (name && content) validExtensions[name] = content;
if (name === "Scripts") foundScripts = true;
}
const [extra, options] = this.#extra_options(mode);
if (extra && options) validExtensions[extra] = options;
return validExtensions;
}
}