dikdimon commited on
Commit
2b0bc5f
·
verified ·
1 Parent(s): cb4d7e0

Upload sd-webui-tabs-extension using SD-Hub

Browse files
sd-webui-tabs-extension/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__
2
+ tab_configs.csv
sd-webui-tabs-extension/.ipynb_checkpoints/tab_configs-checkpoint.csv ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ,txt,img
2
+ default,left,right
3
+ tabs,left,right
4
+ 🎛️ Frequency Separation Enhancement,keep,keep
5
+ 💤 Lazy Pony Prompter,left,right
6
+ Пороги (до 8 правил),keep,keep
7
+ ADetailer,left,right
8
+ Advanced settings,left,right
9
+ AgentAttention,left,right
10
+ Alternate Init Noise,left,left
11
+ Amateur Filter,left,right
12
+ Anti Burn (average smoothing of last steps images),left,right
13
+ Apply licyk style,left,right
14
+ Aspect Ratio to Width/Height,left,right
15
+ Asymmetric tiling,left,right
16
+ Batched Hires (reduce Batch size for Hires. fix),left,left
17
+ BMAB,left,right
18
+ Booru Link to Prompt,left,right
19
+ Booru Tag Inserter,left,right
20
+ Booru to WD,left,right
21
+ CADS,left,right
22
+ Calculate Aspect Ratio,keep,keep
23
+ CD Tuner,left,right
24
+ CFG fade,left,right
25
+ CFG Rescale,keep,keep
26
+ Channel Offset,keep,keep
27
+ Characteristic Guidance (CHG),left,right
28
+ Characteristic Guidance Turbo (CHG),left,right
29
+ Chunk Weight,keep,keep
30
+ Color Correction,left,right
31
+ Colorful Noise,left,right
32
+ Colorization,left,right
33
+ Common Landscape Aspect Ratios,keep,keep
34
+ Composable Lora,left,right
35
+ Compressor,keep,keep
36
+ Cond Blastr,left,right
37
+ Conditioning Highres,left,right
38
+ ControlNet,left,right
39
+ ControlNet-I2I-sequence-toyxyz,left,right
40
+ ControlNet-M2M,left,right
41
+ Cubemap Tiling,left,right
42
+ Custom Hires Fix,keep,keep
43
+ Cutoff,left,right
44
+ DanBooru Link,left,right
45
+ DanTagGen,left,right
46
+ DemoFusion,left,right
47
+ Detail Daemon,left,right
48
+ Diffusion CG,keep,keep
49
+ DiffusionCocktail,left,right
50
+ DyCFG,left,right
51
+ Dynamic Lora Weights,left,right
52
+ Dynamic Thresholding (CFG Scale Fix),left,right
53
+ Extra Samplers,left,right
54
+ FABRIC,left,right
55
+ First Block Cache,left,right
56
+ Forge Couple,left,right
57
+ FreeU,left,right
58
+ Global Prompts,left,right
59
+ HighRes fix: override CFG and Distilled CFG,left,right
60
+ Hires. Fix (txt2img + img2img),left,right
61
+ Image Filters,left,right
62
+ img2img Hires Fix,left,right
63
+ Incantations,left,right
64
+ Inpaint Mask Tools,keep,keep
65
+ Kohya Hires.fix,keep,keep
66
+ Large Initial Thresholding for Forge,left,right
67
+ Latent Couple,left,right
68
+ Latent Drift Correction,left,right
69
+ Latent Mirroring,left,right
70
+ Latent Playground,left,left
71
+ LoRA Block Weight,left,right
72
+ Lora Masks,left,right
73
+ Masa Control,keep,keep
74
+ Merge Options,left,right
75
+ Multiple Hypernetworks,left,right
76
+ NegPiP,left,right
77
+ Neutral Prompt,left,right
78
+ Noise Color Picker,left,left
79
+ Noise scheduler modifier,left,right
80
+ Overlay,left,right
81
+ Parameters info,left,right
82
+ Perturbed Attention Guidance,left,right
83
+ Photo Refiner 2.0,left,right
84
+ picBatchWork,left,right
85
+ Pick CLIP,left,right
86
+ Predefined Aspect Ratios,left,right
87
+ Prompt Refactor,left,right
88
+ Quick Recents,left,right
89
+ Ranbooru,left,right
90
+ Regional Prompter,left,right
91
+ ReSharpen,left,right
92
+ Samplers Scheduler Seniorious,left,right
93
+ ScaleCrafter,keep,keep
94
+ Scheduler Override,left,right
95
+ Scramble Prompts [M9],left,right
96
+ Scripts,keep,keep
97
+ SDXL Latent Tweaking,left,right
98
+ SDXL Resolution Picker,left,right
99
+ SDXL Resolutions,left,right
100
+ SDXL Styles,left,right
101
+ Seed Travel Extras...,left,right
102
+ Self Attention Guidance,left,right
103
+ Semantic Guidance,left,right
104
+ Skimmed CFG,left,right
105
+ Snakeoil CFG for reForge,left,right
106
+ Some Image Effects,left,right
107
+ SSaSD,left,right
108
+ Symmetry,keep,keep
109
+ TeaCache,left,right
110
+ Tiled Diffusion,left,right
111
+ Tiled VAE,left,right
112
+ Trip Clip Skip,left,right
113
+ Tweak Weights [M9],left,right
114
+ Txt2img → Img2img,left,right
115
+ Vectorscope CC,left,right
116
+ Workflow,left,right
117
+ XL Vec,left,right
118
+ Yet Another VAE Tiling,left,right
119
+ µ Detection Detailer,left,right
sd-webui-tabs-extension/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Haoming
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sd-webui-tabs-extension/README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SD Webui Tabs Extension
2
+ This is an Extension for the [Automatic1111 Webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui), which replaces the regular Extension layout with a **Tabs** layout instead.
3
+
4
+ > Compatible with [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge)
5
+
6
+ <p align="center">
7
+ <img src="ui.png" width=768><br>
8
+ <sup>(<a href="https://github.com/Haoming02/catppuccin-theme">Catppuccin Theme</a>)</sup>
9
+ </p>
10
+
11
+ ## Features
12
+ This was achieved by moving the "contents" of each Extension from the "**Accordion**" *(the dropdown thingy)* into their own separate **Tab**. So now you only need to switch between each tab to change the parameters, rather than scrolling all the way every single time.
13
+
14
+ When an Extension is enabled, the respective Tab button will be highlighted in the specified color[*](#settings) to indicate that. Additionally, you can `Ctrl + Click` the Tab button to quickly toggle the Extension.
15
+
16
+ > [!Note]
17
+ > - This tries to find the first **Checkbox** with a label containing "enable" or "active" within the Extension. If one is not found, this feature will not be available for said Extension.
18
+ > - For ControlNet, this only works on the first Unit. Still thinking of a better solution...
19
+
20
+ ## Configs
21
+ After launching the Webui with this Extension installed, it will generate a `tab_configs.csv` file inside the folder. You can edit this file with any text editing program *(**eg.** `Notepad`)* or spreadsheet program *(**eg.** `Excel`)* to change where each element is placed:
22
+
23
+ - <ins><b>entries</b></ins>
24
+ - `tabs` entry controls where the buttons for the Tabs are placed
25
+ - `default` entry controls where new Extensions should be placed
26
+ - `Scripts` entry refers to the Scripts dropdown
27
+ - `Extra Options` entry refers to the `Settings for txt2img/img2img` *(if enabled in settings)*
28
+ - The rest of the entries are the names of your installed extensions
29
+
30
+ - <ins><b>placement</b></ins>
31
+ - `left` side refers to under the parameters *(the original location)*
32
+ - `right` side refers to under the generation gallery
33
+ - `above` refers to above the tab buttons entry
34
+ - `below` refers to below the tab buttons entry
35
+ - `hide` will not show the extension
36
+ - Mainly for extensions with always-active features
37
+ - `keep` will not move the extension at all
38
+ - Mainly for extensions with custom UIs that are not supported:
39
+
40
+ > [!Important]
41
+ > This Extension **only** supports Extensions whose UIs are defined under either `gr.Accordion` or `modules.ui_components.InputAccordion`. If you have some exotic Extensions that ~~like to reinvent the wheels and therefore~~ are not showing up correctly, set it to `keep` instead
42
+
43
+ > [!Important]
44
+ > If you edited the `.csv` file while the Webui is active, you should click **Reload UI** to apply the changes *(not just refresh the browser)*
45
+
46
+ - <ins><b>Example Config:</b></ins>
47
+
48
+ | | txt | img |
49
+ |-------|-----|-----|
50
+ | tabs | left|right|
51
+ |default| left|right|
52
+ |ar-plus|above|above|
53
+ | ... | left|right|
54
+ | ... | keep| keep|
55
+ | ... | left|right|
56
+ |Scripts|below|below|
57
+
58
+ ## Settings
59
+ > The settings are in the **Tabs Extension** section under the <ins>User Interface</ins> category
60
+
61
+ - **Delay:** This Extension moves the contents after a tiny delay to prevent breaking some references
62
+ - If you have a slow machine, you *may* need to increase the delay
63
+ - **Color:** The CSS color used to indicate active extensions *(**default:** `GreenYellow`)*
64
+ - **Version:** Hide the version number for the Extensions *(**not** recommended)*
65
+ - **Forge:** Hide the "Integrated" text for the built-in Extensions
66
+ - **Sort:** By default, extensions are sorted based on their order of appearance *(**ie.** The folder names or the `sorting_priority` variable)*; Enable this to sort them based on the order in the `tab_configs.csv`
67
+ - **Toggle:** Allow you to click on the button of an opened tab to hide it instead
68
+ - **Scripts Toggle:** Additionally allow you to `Ctrl + Click` on the `Scripts` button, to toggle the dropdown between the currently selected script and `None`
69
+ - **RMB:** Use `Right Click` instead of `Ctrl + Click` to toggle the Extensions
70
+ - **Open:** By default, the first extension is shown; Disable this to hide all the extensions on start up instead
71
+ - **Hide Container:** By default, this Extension only moves the contents and hide the Accordions, leaving the extension container untouched. When using certain themes that add margins and paddings, the container may show up as an empty block instead. If you do not have any leftover extensions in the container *(**ie.** Those without Accordion in its UI)*, you can hide the entire container instead.
sd-webui-tabs-extension/javascript/tabs_configs.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class TabsExtensionConfigs {
2
+ #settings = {};
3
+
4
+ /** @returns {boolean} */ get container() { return this.#settings.container; }
5
+ /** @returns {boolean} */ get forge() { return this.#settings.forge; }
6
+ /** @returns {boolean} */ get open() { return this.#settings.open; }
7
+ /** @returns {boolean} */ get sort() { return this.#settings.sort; }
8
+ /** @returns {boolean} */ get toggle() { return this.#settings.toggle; }
9
+ /** @returns {boolean} */ get scripts_toggle() { return this.#settings.scripts_toggle; }
10
+ /** @returns {boolean} */ get rmb() { return this.#settings.rmb; }
11
+ /** @returns {boolean} */ get version() { return this.#settings.version; }
12
+
13
+ constructor() {
14
+ const settingsIDs = [
15
+ ['container', 'checkbox'],
16
+ ['forge', 'checkbox'],
17
+ ['open', 'checkbox'],
18
+ ['sort', 'checkbox'],
19
+ ['toggle', 'checkbox'],
20
+ ['scripts_toggle', 'checkbox'],
21
+ ['rmb', 'checkbox'],
22
+ ['version', 'checkbox']
23
+ ];
24
+
25
+ for (const [key, type] of settingsIDs) {
26
+ const wrapper = document.getElementById(`setting_tabs_ex_${key}`);
27
+ const element = wrapper?.querySelector(`input[type=${type}]`);
28
+ this.#settings[key] = (type === 'checkbox') ? !!element?.checked : (element?.value ?? "");
29
+ }
30
+ }
31
+
32
+ /** @param {string} name @param {string} mode @return {string} */
33
+ #validateMode(name, mode) {
34
+ const availableModes = (name === "tabs")
35
+ ? ["left", "right"]
36
+ : ["left", "right", "above", "below", "hide", "keep"];
37
+ return availableModes.includes(mode) ? mode : availableModes[0];
38
+ }
39
+
40
+ /**
41
+ * CSV -> Dict
42
+ * @return {Object.<string, Object.<string, string>>}
43
+ */
44
+ parseConfigs() {
45
+ try {
46
+ const config = { "txt": {}, "img": {} };
47
+ const holder = document.getElementById('TABSEX_LBL');
48
+ const textarea = holder?.querySelector('textarea');
49
+ const raw = textarea?.value ?? "";
50
+
51
+ const lines = raw.trim().split('\n').slice(1).filter(Boolean);
52
+ for (const line of lines) {
53
+ const [ext, t, i] = line.split(',').map(col => (col ?? '').trim());
54
+ if (!ext) continue;
55
+ config["txt"][ext] = this.#validateMode(ext, t);
56
+ config["img"][ext] = this.#validateMode(ext, i);
57
+ }
58
+
59
+ // пусто? вернём дефолт
60
+ if (Object.keys(config.txt ?? {}).length === 0) {
61
+ return {
62
+ "txt": { 'tabs': 'left', 'default': 'left' },
63
+ "img": { 'tabs': 'right', 'default': 'right' }
64
+ };
65
+ }
66
+ return config;
67
+ } catch (_e) {
68
+ alert(`[Tabs Extension]: Something went wrong while parsing the configs... Using defaults.`);
69
+ return {
70
+ "txt": { 'tabs': 'left', 'default': 'left' },
71
+ "img": { 'tabs': 'right', 'default': 'right' }
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Dict -> CSV (фиксированный порядок)
78
+ * @param {Object.<string, Object.<string, string>>} config
79
+ */
80
+ saveConfigs(config) {
81
+ const holder = document.getElementById('TABSEX_LBL');
82
+ const textarea = holder?.querySelector('textarea');
83
+ if (!textarea) return;
84
+
85
+ const data = [",txt,img"];
86
+
87
+ const keys = Array.from(new Set([
88
+ ...Object.keys(config["txt"] || {}),
89
+ ...Object.keys(config["img"] || {})
90
+ ])).sort((a, b) => {
91
+ const aHead = (a === 'tabs' || a === 'default');
92
+ const bHead = (b === 'tabs' || b === 'default');
93
+ if (aHead && !bHead) return -1;
94
+ if (!aHead && bHead) return 1;
95
+ return a.localeCompare(b);
96
+ });
97
+
98
+ for (const key of keys) {
99
+ data.push([key, config["txt"][key] ?? "", config["img"][key] ?? ""].join(","));
100
+ }
101
+
102
+ textarea.value = data.join("\n");
103
+ if (typeof updateInput === 'function') updateInput(textarea);
104
+
105
+ const btn = document.getElementById('TABSEX_BTN');
106
+ btn?.click();
107
+ }
108
+ }
sd-webui-tabs-extension/javascript/tabs_ex.js ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TabsExtension — организация вкладок расширений + поиск по вкладкам
2
+ // ECMAScript 2020+, A1111/Forge совместим
3
+
4
+ class TabsExtension {
5
+
6
+ /** @type {TabsExtensionConfigs} */
7
+ static #config;
8
+
9
+ /** Активные вкладки по режимам */
10
+ /** @type {Object.<string, string|null>} */
11
+ static #activeExtension = { "txt": null, "img": null };
12
+
13
+ /** Пары "чекбокс включения" <-> "кнопка вкладки" для подсветки active */
14
+ /** @type {Array.<[HTMLInputElement, HTMLButtonElement]>} */
15
+ static #enablePairs = [];
16
+
17
+ /** Таймер отложенного обновления подсветки active */
18
+ /** @type {number|null} */
19
+ static #refreshQueue = null;
20
+
21
+ /** @param {HTMLDivElement} extension @returns {HTMLInputElement|null} */
22
+ static #tryFindEnableToggle(extension) {
23
+ let temp = null;
24
+ for (const checkbox of extension.querySelectorAll('input[type=checkbox]')) {
25
+ const labelText = checkbox.parentNode?.querySelector('span')?.textContent?.toLowerCase();
26
+ if (labelText?.includes('enable')) return checkbox;
27
+ if (!temp && labelText?.includes('active')) temp = checkbox;
28
+ }
29
+ return temp;
30
+ }
31
+
32
+ /** Синхронизация active-подсветки кнопок с реальным состоянием чекбоксов */
33
+ static #refreshEnableCheckbox() {
34
+ if (this.#refreshQueue) clearTimeout(this.#refreshQueue);
35
+ this.#refreshQueue = window.setTimeout(() => {
36
+ for (const [toggle, button] of this.#enablePairs) {
37
+ if (toggle.checked) button.classList.add('active');
38
+ else button.classList.remove('active');
39
+ }
40
+ }, 250);
41
+ }
42
+
43
+ /**
44
+ * Сортировка расширений по конфигу, если включено
45
+ * @param {Object.<string, HTMLDivElement>} extensions
46
+ * @param {Object.<string, string>} configs
47
+ * @returns {Object.<string, HTMLDivElement>}
48
+ */
49
+ static #sort_extensions(extensions, configs) {
50
+ if (!this.#config.sort) return extensions;
51
+ const sorted = {};
52
+ for (const key of Object.keys(configs)) {
53
+ if (Object.prototype.hasOwnProperty.call(extensions, key)) {
54
+ sorted[key] = extensions[key];
55
+ delete extensions[key];
56
+ }
57
+ }
58
+ for (const key of Object.keys(extensions)) sorted[key] = extensions[key];
59
+ return sorted;
60
+ }
61
+
62
+ /**
63
+ * Главная раскладка
64
+ * @param {'txt'|'img'} mode
65
+ * @param {Object.<string, HTMLDivElement>} extensions
66
+ * @param {Object.<string, string>} configs
67
+ * @returns {Object.<string, string>}
68
+ */
69
+ static #setup_tabs(mode, extensions, configs) {
70
+ const container = {
71
+ 'left': document.getElementById(`${mode}2img_script_container`),
72
+ 'right': document.getElementById(`${mode}2img_results`)
73
+ };
74
+
75
+ /** @type {string} */ const mainSide = configs['tabs'];
76
+ /** @type {string} */ const oppSide = (mainSide === 'left') ? 'right' : 'left';
77
+
78
+ // Если вдруг контейнеров нет — выходим аккуратно
79
+ if (!container[mainSide] || !container[oppSide]) return configs;
80
+
81
+ // Заготовим области
82
+ const extensionContainers = {};
83
+ for (const side of ['above', 'left', 'below', 'right']) {
84
+ const div = document.createElement("div");
85
+ div.id = `tabs_ex_content-${mode}2img-${side}`;
86
+ div.style.overflow = "visible";
87
+ extensionContainers[side] = div;
88
+ }
89
+
90
+ const buttonContainer = document.createElement("div");
91
+ extensionContainers[mainSide].appendChild(buttonContainer);
92
+ buttonContainer.id = `tabs_ex_${mode}`;
93
+
94
+ container[mainSide].appendChild(extensionContainers['above']);
95
+ container[mainSide].appendChild(extensionContainers[mainSide]);
96
+ container[mainSide].appendChild(extensionContainers['below']);
97
+ container[oppSide].appendChild(extensionContainers[oppSide]);
98
+
99
+ /** Карта: ключ вкладки -> кнопка */
100
+ /** @type {Object.<string, HTMLButtonElement>} */
101
+ const allButtons = {};
102
+
103
+ // === Поиск по вкладкам (компактный) ===
104
+ const searchWrapper = document.createElement("div");
105
+ searchWrapper.style.display = "flex";
106
+ searchWrapper.style.justifyContent = "center";
107
+ searchWrapper.style.width = "100%";
108
+ searchWrapper.style.marginBottom = "6px";
109
+
110
+ const searchInput = document.createElement("input");
111
+ searchInput.type = "text";
112
+ searchInput.placeholder = "Filter extensions...";
113
+ searchInput.autocomplete = "off";
114
+ searchInput.style.width = "100%";
115
+ searchInput.style.maxWidth = "480px"; // чтобы не расползался как на скрине
116
+ searchInput.style.minWidth = "240px";
117
+ searchInput.style.padding = "6px 10px";
118
+ searchInput.style.borderRadius = "6px";
119
+ searchInput.style.border = "1px solid var(--block-border-color, #444)";
120
+
121
+ searchWrapper.appendChild(searchInput);
122
+ buttonContainer.appendChild(searchWrapper);
123
+
124
+ const self = this; // нужен доступ к #activeExtension внутри коллбэка
125
+ const applyFilter = (queryText) => {
126
+ const q = (queryText || "").trim().toLowerCase();
127
+ for (const [tabKey, btn] of Object.entries(allButtons)) {
128
+ const label = (btn.textContent || "").toLowerCase();
129
+ btn.style.display = (q.length === 0 || label.includes(q)) ? "" : "none";
130
+ }
131
+ // если активная вкладка скрыта фильтром — мягко гасим её контент
132
+ const active = self.#activeExtension[mode];
133
+ if (active && allButtons[active] && allButtons[active].style.display === "none") {
134
+ allButtons[active].classList.remove('selected');
135
+ if (extensions[active]) extensions[active].style.display = "none";
136
+ self.#activeExtension[mode] = null;
137
+ }
138
+ };
139
+ searchInput.addEventListener("input", (e) => applyFilter(e.target.value));
140
+
141
+ // Кнопки и раскладка
142
+ for (const tabKey of Object.keys(extensions)) {
143
+ if (!Object.prototype.hasOwnProperty.call(configs, tabKey)) {
144
+ configs[tabKey] = configs['default']; // новое расширение — дефолтная зона
145
+ }
146
+
147
+ const pos = configs[tabKey];
148
+ if (pos === "hide") continue;
149
+
150
+ if (pos === "above" || pos === "below") {
151
+ extensionContainers[pos].appendChild(extensions[tabKey]);
152
+ extensions[tabKey].style.display = "block";
153
+ continue;
154
+ }
155
+
156
+ // Подпись кнопки
157
+ const btnSpan = document.createElement('span');
158
+ btnSpan.className = 'tab_label';
159
+
160
+ const extensionName = (this.#config.version)
161
+ ? tabKey
162
+ : extensions[tabKey].getAttribute("ext-label");
163
+
164
+ btnSpan.textContent = (!this.#config.forge)
165
+ ? extensionName
166
+ : (extensionName || "").split('Integrated')[0].trim();
167
+
168
+ // Кнопка вкладки
169
+ const tabButton = document.createElement("button");
170
+ tabButton.classList.add('tab_button');
171
+ tabButton.appendChild(btnSpan);
172
+
173
+ buttonContainer.appendChild(tabButton);
174
+ allButtons[tabKey] = tabButton;
175
+
176
+ tabButton.addEventListener("click", (e) => {
177
+ // режим RMB: Ctrl/Cmd не перехватываем
178
+ if (!this.#config.rmb && (e.ctrlKey || e.metaKey)) return;
179
+
180
+ if (this.#activeExtension[mode] != null) {
181
+ allButtons[this.#activeExtension[mode]]?.classList.remove('selected');
182
+ const prev = this.#activeExtension[mode];
183
+ if (prev && extensions[prev]) extensions[prev].style.display = "none";
184
+ }
185
+
186
+ this.#activeExtension[mode] = (
187
+ (this.#config.toggle) && (this.#activeExtension[mode] === tabKey)
188
+ ) ? null : tabKey;
189
+
190
+ if (this.#activeExtension[mode] != null) {
191
+ allButtons[this.#activeExtension[mode]]?.classList.add('selected');
192
+ extensions[this.#activeExtension[mode]].style.display = "block";
193
+ }
194
+ });
195
+
196
+ // Переносим содержимое вкладки в нужную сторону
197
+ extensionContainers[configs[tabKey]].appendChild(extensions[tabKey]);
198
+
199
+ // Особый случай: Scripts
200
+ if (tabKey === 'Scripts') {
201
+ const scriptsDropdown = extensions[tabKey].querySelector('input');
202
+
203
+ const observer = new MutationObserver(() => {
204
+ if (!scriptsDropdown) return;
205
+ if (scriptsDropdown.value !== 'None') allButtons['Scripts']?.classList.add('active');
206
+ else allButtons['Scripts']?.classList.remove('active');
207
+ });
208
+ observer.observe(extensions[tabKey], { childList: true, subtree: true });
209
+
210
+ if (this.#config.scripts_toggle) {
211
+ const btn = document.getElementById(`TABSEX_${mode}2img_s_toggle`);
212
+ if (btn) {
213
+ if (this.#config.rmb) {
214
+ allButtons[tabKey].addEventListener("contextmenu", (e) => {
215
+ e.preventDefault(); btn.click(); return false;
216
+ });
217
+ } else {
218
+ allButtons[tabKey].addEventListener("click", (e) => {
219
+ if (e.ctrlKey || e.metaKey) btn.click();
220
+ });
221
+ }
222
+ }
223
+ }
224
+ continue;
225
+ }
226
+
227
+ // Подсветка активных по чекбоксу Enable/Active
228
+ const enableToggle = this.#tryFindEnableToggle(extensions[tabKey]);
229
+ if (!enableToggle) continue;
230
+
231
+ if (this.#config.rmb) {
232
+ allButtons[tabKey].addEventListener("contextmenu", (e) => {
233
+ e.preventDefault(); enableToggle.click(); return false;
234
+ });
235
+ } else {
236
+ allButtons[tabKey].addEventListener("click", (e) => {
237
+ if (e.ctrlKey || e.metaKey) enableToggle.click();
238
+ });
239
+ }
240
+
241
+ if (enableToggle.checked) allButtons[tabKey].classList.add('active');
242
+ this.#enablePairs.push([enableToggle, allButtons[tabKey]]);
243
+ }
244
+
245
+ // Чистим пустые области
246
+ for (const side of ['above', 'left', 'below', 'right']) {
247
+ if (extensionContainers[side].children.length === 0) {
248
+ extensionContainers[side].remove();
249
+ }
250
+ }
251
+
252
+ if (this.#config.open) {
253
+ const firstBtn = Object.values(allButtons)[0];
254
+ if (firstBtn) firstBtn.click();
255
+ }
256
+
257
+ return configs;
258
+ }
259
+
260
+ /** Точка входа */
261
+ static init() {
262
+ this.#config = new TabsExtensionConfigs();
263
+ const configs = this.#config.parseConfigs();
264
+ const processedConfigs = {};
265
+
266
+ for (const mode of ['txt', 'img']) {
267
+ let extensions = null;
268
+
269
+ const keepExtensions = Object.keys(configs[mode])
270
+ .filter((ext) => configs[mode][ext] === "keep");
271
+
272
+ try {
273
+ const parsed = TabsExtensionParser.parse(mode, keepExtensions);
274
+ extensions = this.#sort_extensions(parsed, configs[mode]);
275
+ } catch (e) {
276
+ alert(`[TabsExtension] Something went wrong while parsing ${mode}2img extensions:\n${e}`);
277
+ continue;
278
+ }
279
+
280
+ try {
281
+ processedConfigs[mode] = this.#setup_tabs(mode, extensions, configs[mode]);
282
+ if (this.#config.container) {
283
+ const styler = document.getElementById(`${mode}2img_script_container`)
284
+ ?.querySelector(".styler");
285
+ if (styler) styler.style.display = "none";
286
+ }
287
+ } catch (e) {
288
+ alert(`[TabsExtension] Something went wrong during ${mode}2img setup:\n${e}`);
289
+ continue;
290
+ }
291
+ }
292
+
293
+ try {
294
+ this.#config.saveConfigs(processedConfigs);
295
+ } catch (e) {
296
+ alert(`[TabsExtension] Something went wrong while saving configs:\n${e}`);
297
+ }
298
+
299
+ document.addEventListener("click", () => this.#refreshEnableCheckbox());
300
+ }
301
+ }
302
+
303
+ // Инициализация после загрузки UI
304
+ (function () {
305
+ onUiLoaded(() => {
306
+ const slider = document.getElementById("setting_tabs_ex_delay")
307
+ ?.querySelector("input[type=range]");
308
+ const delay = parseInt(slider?.value ?? "50", 10);
309
+
310
+ setTimeout(() => {
311
+ if (typeof TabsExtension?.init === "function") TabsExtension.init();
312
+ }, Number.isNaN(delay) ? 50 : delay);
313
+ });
314
+ })();
sd-webui-tabs-extension/javascript/tabs_parser.js ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ECMAScript 2020+
2
+ // Разбор расширений и подготовка для табов
3
+
4
+ class TabsExtensionParser {
5
+
6
+ /** Клонируем label с чекбоксом и возвращаем именно label (или null)
7
+ * @param {boolean} enabled
8
+ * @returns {HTMLLabelElement|null}
9
+ */
10
+ static #cloneCheckbox(enabled) {
11
+ const base = document.getElementById("TABSEX_CHECKBOX");
12
+ if (!base) return null;
13
+
14
+ const label = base.querySelector('label')?.cloneNode(true);
15
+ if (!label) return null;
16
+
17
+ label.style.margin = "1em 0em";
18
+ label.checkbox = label.querySelector("input");
19
+ if (label.checkbox) label.checkbox.checked = !!enabled;
20
+ return label;
21
+ }
22
+
23
+ /**
24
+ * Убираем версию из имени (если есть). Пустые строки -> null.
25
+ * @param {string} name
26
+ * @returns {string|null}
27
+ */
28
+ static #sanitizeExtensionName(name) {
29
+ if (typeof name !== 'string' || name.trim().length === 0) return null;
30
+ const version_pattern = /([Vv](?:er)?[\.\s]*\d)/;
31
+ return name.split(version_pattern)[0].trim();
32
+ }
33
+
34
+ /**
35
+ * Разбор одного узла из контейнера расширений
36
+ * @param {HTMLDivElement} node
37
+ * @param {string[]} keep
38
+ * @returns {[string|null, HTMLDivElement|null]}
39
+ */
40
+ static #parseObject(node, keep) {
41
+ if (!node) return [null, null];
42
+
43
+ // Спец-случай: форма со списком скриптов
44
+ if (node.classList.contains("form")) {
45
+ const scripts = node.querySelector(".gradio-dropdown");
46
+ if (!scripts) return [null, null];
47
+
48
+ const script_block = document.createElement("div");
49
+ script_block.style.display = 'none';
50
+ scripts.style.margin = '10px 0px';
51
+ script_block.appendChild(scripts);
52
+
53
+ script_block.setAttribute("ext-label", "Scripts");
54
+ // прячем исходный form-узел, чтобы не было дубля; решение о дубликате принимает parse()
55
+ node.style.display = "none";
56
+ return ["Scripts", script_block];
57
+ }
58
+
59
+ const styler = node.querySelector(".styler");
60
+ if (!styler) return [null, null];
61
+
62
+ const accordion = node.querySelector(".gradio-accordion");
63
+ if (!accordion) return [null, null];
64
+
65
+ const isInput = accordion.classList.contains("input-accordion");
66
+
67
+ const displayName = accordion.querySelector(".label-wrap>span")?.textContent;
68
+ if (!displayName) return [null, null];
69
+
70
+ const extensionName = this.#sanitizeExtensionName(displayName);
71
+ if (!extensionName || keep.includes(extensionName)) return [null, null];
72
+
73
+ const contents = [...accordion.children].filter(
74
+ (div) => !div.classList.contains("hide")
75
+ && !div.classList.contains("label-wrap")
76
+ && div.children.length > 0
77
+ );
78
+
79
+ if (contents.length === 0) return [null, null];
80
+
81
+ const content = contents[0];
82
+
83
+ if (isInput) {
84
+ const checkbox = accordion.querySelector(".input-accordion-checkbox");
85
+ const dummy = this.#cloneCheckbox(checkbox?.checked ?? false);
86
+
87
+ // Ставим обработчики только если всё есть
88
+ if (dummy && dummy.checkbox && checkbox) {
89
+ dummy.checkbox.onchange = () => {
90
+ if (checkbox.checked !== dummy.checkbox.checked) checkbox.click();
91
+ };
92
+ checkbox.onchange = () => {
93
+ if (checkbox.checked !== dummy.checkbox.checked) dummy.checkbox.click();
94
+ };
95
+ content.insertBefore(dummy, content.firstElementChild);
96
+ }
97
+ }
98
+
99
+ // Прячем исходный блок с аккордеоном
100
+ node.style.display = "none";
101
+
102
+ // Переносим якорь на новый content (и переименовываем старый id безопасно)
103
+ if (accordion.id && !accordion.id.startsWith("component-")) {
104
+ content.id = accordion.id;
105
+ accordion.id = `moved-${accordion.id}`;
106
+
107
+ if (isInput) {
108
+ // Предотвращаем ошибки в консоли в сетапе webui
109
+ content.visibleCheckbox = { checked: null };
110
+ content.onVisibleCheckboxChange = () => {};
111
+ }
112
+ }
113
+
114
+ content.setAttribute("ext-label", displayName);
115
+ return [extensionName, content];
116
+ }
117
+
118
+ /**
119
+ * Доп. блок "Extra Options"
120
+ * @param {'txt'|'img'} mode
121
+ * @returns {[string|null, HTMLDivElement|null]}
122
+ */
123
+ static #extra_options(mode) {
124
+ const extra_options = document.getElementById(`extra_options_${mode}2img`);
125
+ if (!extra_options || !extra_options.classList.contains("gradio-accordion"))
126
+ return [null, null];
127
+
128
+ const styler = extra_options.parentElement;
129
+ if (styler) styler.style.display = "none";
130
+
131
+ const contents = [...extra_options.children].filter(
132
+ (div) => !div.classList.contains("hide")
133
+ && !div.classList.contains("label-wrap")
134
+ && div.children.length > 0
135
+ );
136
+ if (contents.length === 0) return [null, null];
137
+
138
+ const content = contents[0];
139
+ content.setAttribute("ext-label", "Extra Options");
140
+ return ["Extra Options", content];
141
+ }
142
+
143
+ /**
144
+ * Разбор всех расширений
145
+ * @param {'txt'|'img'} mode
146
+ * @param {string[]} keep
147
+ * @returns {Object.<string, HTMLDivElement>}
148
+ */
149
+ static parse(mode, keep) {
150
+ const validExtensions = {};
151
+
152
+ const container = document.getElementById(`${mode}2img_script_container`);
153
+ const styler = container?.querySelector(".styler");
154
+ if (!container || !styler) return validExtensions;
155
+
156
+ const children = Array.from(styler.children);
157
+ let foundScripts = false; // флаг "Scripts уже встретился" в ЭТОМ контейнере
158
+
159
+ // Важно: НИЧЕГО больше не «скидываем» в Scripts — разбираем каждый узел отдельно.
160
+ for (const node of children) {
161
+ // не допускаем второй Scripts в пределах текущего контейнера
162
+ if (foundScripts && node.classList.contains("form")) {
163
+ node.style.display = "none";
164
+ continue;
165
+ }
166
+ const [name, content] = this.#parseObject(node, keep);
167
+ if (name && content) validExtensions[name] = content;
168
+ if (name === "Scripts") foundScripts = true;
169
+ }
170
+
171
+ const [extra, options] = this.#extra_options(mode);
172
+ if (extra && options) validExtensions[extra] = options;
173
+
174
+ return validExtensions;
175
+ }
176
+ }
sd-webui-tabs-extension/scripts/__pycache__/tabs_config_io.cpython-310.pyc ADDED
Binary file (3.88 kB). View file
 
sd-webui-tabs-extension/scripts/__pycache__/tabs_settings.cpython-310.pyc ADDED
Binary file (3.32 kB). View file
 
sd-webui-tabs-extension/scripts/tabs_config_io.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.shared import opts
2
+ from modules import scripts
3
+ import gradio as gr
4
+ import os.path
5
+
6
+
7
+ CONFIG_FILE = os.path.join(scripts.basedir(), "tab_configs.csv")
8
+
9
+
10
+ class TabsEx(scripts.Script):
11
+ sorting_priority = 8192
12
+ data: str = None
13
+
14
+ s_toggles: list[gr.components.Component] = None
15
+ t2i_done: bool = False
16
+
17
+ def __init__(self):
18
+ if TabsEx.data is None:
19
+ TabsEx.data = TabsEx._load_data()
20
+
21
+ def title(self):
22
+ return "Tabs Extension"
23
+
24
+ def show(self, is_img2img):
25
+ return None if is_img2img else scripts.AlwaysVisible
26
+
27
+ def ui(self, is_img2img):
28
+ if is_img2img:
29
+ return None
30
+
31
+ if getattr(opts, "tabs_ex_scripts_toggle", False):
32
+ with gr.Group(visible=False):
33
+ TabsEx.s_toggles = [
34
+ gr.Button("T", elem_id="TABSEX_txt2img_s_toggle", interactive=True),
35
+ gr.State(value="None"),
36
+ gr.Button("T", elem_id="TABSEX_img2img_s_toggle", interactive=True),
37
+ gr.State(value="None"),
38
+ ]
39
+
40
+ for comp in TabsEx.s_toggles:
41
+ comp.do_not_save_to_config = True
42
+
43
+ with gr.Column(visible=False):
44
+ dummy = gr.Checkbox(
45
+ label="Enable",
46
+ elem_id="TABSEX_CHECKBOX",
47
+ interactive=True,
48
+ )
49
+ label = gr.Textbox(
50
+ label="[TabsExtension] Configs",
51
+ elem_id="TABSEX_LBL",
52
+ value=TabsEx.data,
53
+ )
54
+
55
+ btn = gr.Button(value="save", elem_id="TABSEX_BTN")
56
+ btn.click(fn=self._write_data, inputs=[label], queue=False)
57
+
58
+ [setattr(comp, "do_not_save_to_config", True) for comp in (dummy, label, btn)]
59
+ return None
60
+
61
+ @staticmethod
62
+ def _load_data() -> str:
63
+ if not os.path.isfile(CONFIG_FILE):
64
+ return "\n".join(
65
+ [
66
+ ",".join(["", "txt", "img"]),
67
+ ",".join(["tabs", "left", "right"]),
68
+ ",".join(["default", "left", "right"]),
69
+ ]
70
+ )
71
+
72
+ with open(CONFIG_FILE, "r", encoding="utf-8", errors="ignore") as csv_file:
73
+ return csv_file.read()
74
+
75
+ def _write_data(self, data: str):
76
+ try:
77
+ if data.strip() != TabsEx.data.strip():
78
+ print("\n[TabsExtension] Saving New Config...\n")
79
+ with open(CONFIG_FILE, "w+", encoding="utf-8") as csv_file:
80
+ csv_file.write(data)
81
+ except Exception:
82
+ raise gr.Error("[TabsExtension] Failed to save config...")
83
+
84
+
85
+ if getattr(opts, "tabs_ex_scripts_toggle", False):
86
+ from modules.script_callbacks import on_after_component
87
+
88
+ def after_component(component, **kwargs):
89
+ if kwargs.get("elem_id", None) != "script_list":
90
+ return
91
+
92
+ # на некоторых темах/сборках "script_list" может отличаться — не падаем
93
+ if not isinstance(component, gr.Dropdown):
94
+ return
95
+
96
+ if TabsEx.t2i_done is True:
97
+ btn, c = TabsEx.s_toggles[2:4]
98
+ ss: list = scripts.scripts_img2img.selectable_scripts
99
+ else:
100
+ btn, c = TabsEx.s_toggles[0:2]
101
+ ss: list = scripts.scripts_txt2img.selectable_scripts
102
+ TabsEx.t2i_done = True
103
+
104
+ def _toggle(current: int, cache: str):
105
+ if not current or current == "None":
106
+ _a: str = cache
107
+ _b: str = "None"
108
+ else:
109
+ _a: str = "None"
110
+ _b: str = ss[current - 1].title()
111
+ return [_a, _b]
112
+
113
+ btn.click(
114
+ fn=_toggle,
115
+ inputs=[component, c],
116
+ outputs=[component, c],
117
+ show_progress="hidden",
118
+ )
119
+
120
+ on_after_component(after_component)
sd-webui-tabs-extension/scripts/tabs_settings.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.script_callbacks import on_ui_settings, on_before_ui
2
+ from modules.shared import opts, OptionInfo
3
+
4
+
5
+ def add_ui_settings():
6
+ from gradio import Slider
7
+
8
+ args = {"section": ("ui_tabs_ex", "Tabs Extension"), "category_id": "ui"}
9
+
10
+ opts.add_option(
11
+ "tabs_ex_delay",
12
+ OptionInfo(
13
+ 50,
14
+ "Delay (ms) before moving the Extensions",
15
+ Slider,
16
+ {"minimum": 0, "maximum": 500, "step": 25},
17
+ **args,
18
+ ).needs_reload_ui(),
19
+ )
20
+
21
+ opts.add_option(
22
+ "tabs_ex_act_color",
23
+ OptionInfo(
24
+ "greenyellow",
25
+ "Color for active Extensions",
26
+ **args,
27
+ )
28
+ .link("CSS", "https://www.w3schools.com/cssref/css_colors.php")
29
+ .needs_reload_ui(),
30
+ )
31
+
32
+ opts.add_option(
33
+ "tabs_ex_version",
34
+ OptionInfo(
35
+ False,
36
+ "Hide the version number",
37
+ **args,
38
+ ).needs_reload_ui(),
39
+ )
40
+
41
+ opts.add_option(
42
+ "tabs_ex_forge",
43
+ OptionInfo(
44
+ False,
45
+ 'Hide the "Integrated" text',
46
+ **args,
47
+ )
48
+ .info('for <a href="https://github.com/lllyasviel/stable-diffusion-webui-forge">Forge</a>')
49
+ .needs_reload_ui(),
50
+ )
51
+
52
+ opts.add_option(
53
+ "tabs_ex_sort",
54
+ OptionInfo(
55
+ False,
56
+ "Sort Extensions based on Configs",
57
+ **args,
58
+ ).needs_reload_ui(),
59
+ )
60
+
61
+ opts.add_option(
62
+ "tabs_ex_toggle",
63
+ OptionInfo(
64
+ False,
65
+ "Allow hiding the extension content when clicking on the same tab button again",
66
+ **args,
67
+ ).needs_reload_ui(),
68
+ )
69
+
70
+ opts.add_option(
71
+ "tabs_ex_scripts_toggle",
72
+ OptionInfo(
73
+ False,
74
+ "Allow Ctrl + Click to toggle the Scripts dropdown as well",
75
+ **args,
76
+ ).needs_reload_ui(),
77
+ )
78
+
79
+ opts.add_option(
80
+ "tabs_ex_rmb",
81
+ OptionInfo(
82
+ False,
83
+ "Use Right Click instead of Ctrl + Click for toggling Extensions",
84
+ **args,
85
+ ).needs_reload_ui(),
86
+ )
87
+
88
+ opts.add_option(
89
+ "tabs_ex_open",
90
+ OptionInfo(
91
+ True,
92
+ "Automatically show the first extension tab on startup",
93
+ **args,
94
+ ).needs_reload_ui(),
95
+ )
96
+
97
+ opts.add_option(
98
+ "tabs_ex_container",
99
+ OptionInfo(
100
+ False,
101
+ "Hide the Extension container",
102
+ **args,
103
+ )
104
+ .info("In certain configurations, the original Extension container will show up as an empty space in the Webui. You can enable this to hide the container")
105
+ .needs_reload_ui(),
106
+ )
107
+
108
+
109
+ def load_ui_settings():
110
+ import os
111
+
112
+ color = getattr(opts, "tabs_ex_act_color", "greenyellow").strip().lower()
113
+
114
+ css_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "style.css")
115
+ if not os.path.isfile(css_path):
116
+ # Создаём минимальный файл с нужной переменной
117
+ with open(css_path, "w", encoding="utf-8") as f:
118
+ f.write("body {\n --tabs-highlight-color: %s;\n}\n" % color)
119
+ return
120
+
121
+ with open(css_path, "r", encoding="utf-8", errors="ignore") as f:
122
+ lines = f.readlines()
123
+
124
+ key = "--tabs-highlight-color:"
125
+ idx = next((i for i, line in enumerate(lines) if key in line), None)
126
+
127
+ if idx is not None:
128
+ prefix = lines[idx].split(":")[0]
129
+ lines[idx] = f"{prefix}: {color};\n"
130
+ else:
131
+ # Если строки нет — добавляем под body либо в конец
132
+ insert_at = len(lines)
133
+ for i, line in enumerate(lines):
134
+ if line.strip().startswith("body"):
135
+ insert_at = i + 1
136
+ break
137
+ lines.insert(insert_at, f" --tabs-highlight-color: {color};\n")
138
+
139
+ with open(css_path, "w", encoding="utf-8") as f:
140
+ f.writelines(lines)
141
+
sd-webui-tabs-extension/style.css ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ --tabs-highlight-color: greenyellow;
3
+ }
4
+
5
+ #tabs_ex_txt, #tabs_ex_img {
6
+ display: flex;
7
+ flex-wrap: wrap;
8
+ flex-direction: row;
9
+ align-items: center;
10
+ align-self: center;
11
+ justify-content: center;
12
+ margin: 8px 2px;
13
+ padding: 6px;
14
+ border-radius: 8px;
15
+ background-color: var(--background-fill-secondary);
16
+ }
17
+
18
+ #tabs_ex_txt .tab_button, #tabs_ex_img .tab_button {
19
+ margin: 4px;
20
+ padding: 2px 6px;
21
+ border: 2px;
22
+ border-style: ridge;
23
+ color: var(--button-secondary-text-color);
24
+ border-color: var(--block-info-text-color);
25
+ background-color: transparent;
26
+ transition: all 0.1s linear;
27
+ max-width: 240px; /* не расползаться чрезмерно */
28
+ white-space: nowrap; /* не переносить названия на 2 строки */
29
+ overflow: hidden; /* скрыть хвост */
30
+ text-overflow: ellipsis; /* троеточие для очень длинных имён */
31
+ }
32
+
33
+ #tabs_ex_txt .tab_button:hover, #tabs_ex_img .tab_button:hover {
34
+ box-shadow: 0px 0px 8px var(--button-secondary-text-color);
35
+ }
36
+
37
+ #tabs_ex_txt .tab_button.selected, #tabs_ex_img .tab_button.selected {
38
+ background-color: var(--border-color-accent);
39
+ }
40
+
41
+ #tabs_ex_txt .tab_button.active, #tabs_ex_img .tab_button.active {
42
+ color: var(--tabs-highlight-color);
43
+
44
+ /* Simulate Bold, without affecting the Width */
45
+ text-shadow: 0px 0px 2px var(--tabs-highlight-color);
46
+ }
47
+
48
+ #tabs_ex_content-txt2img-left,
49
+ #tabs_ex_content-txt2img-right,
50
+ #tabs_ex_content-txt2img-above,
51
+ #tabs_ex_content-txt2img-below,
52
+ #tabs_ex_content-img2img-left,
53
+ #tabs_ex_content-img2img-right,
54
+ #tabs_ex_content-img2img-above,
55
+ #tabs_ex_content-img2img-below {
56
+ padding: 1em;
57
+ background-color: var(--block-background-fill);
58
+ border-radius: 8px;
59
+ }
sd-webui-tabs-extension/tab_configs.csv ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ,txt,img
2
+ default,left,right
3
+ tabs,left,right
4
+ 🎛️ Frequency Separation Enhancement,keep,keep
5
+ 💤 Lazy Pony Prompter,left,right
6
+ Пороги (до 8 правил),keep,keep
7
+ ADetailer,left,right
8
+ Advanced settings,left,right
9
+ AgentAttention,left,right
10
+ Alternate Init Noise,left,left
11
+ Amateur Filter,left,right
12
+ Anti Burn (average smoothing of last steps images),left,right
13
+ Apply licyk style,left,right
14
+ Aspect Ratio to Width/Height,left,right
15
+ Asymmetric tiling,left,right
16
+ Batched Hires (reduce Batch size for Hires. fix),left,left
17
+ BMAB,left,right
18
+ Booru Link to Prompt,left,right
19
+ Booru Tag Inserter,left,right
20
+ Booru to WD,left,right
21
+ CADS,left,right
22
+ Calculate Aspect Ratio,keep,keep
23
+ CD Tuner,left,right
24
+ CFG fade,left,right
25
+ CFG Rescale,keep,keep
26
+ Channel Offset,keep,keep
27
+ Characteristic Guidance (CHG),left,right
28
+ Characteristic Guidance Turbo (CHG),left,right
29
+ Chunk Weight,keep,keep
30
+ Color Correction,left,right
31
+ Colorful Noise,left,right
32
+ Colorization,left,right
33
+ Common Landscape Aspect Ratios,keep,keep
34
+ Composable Lora,left,right
35
+ Compressor,keep,keep
36
+ Cond Blastr,left,right
37
+ Conditioning Highres,left,right
38
+ ControlNet,left,right
39
+ ControlNet-I2I-sequence-toyxyz,left,right
40
+ ControlNet-M2M,left,right
41
+ Cubemap Tiling,left,right
42
+ Custom Hires Fix,keep,keep
43
+ Cutoff,left,right
44
+ DanBooru Link,left,right
45
+ DanTagGen,left,right
46
+ DemoFusion,left,right
47
+ Detail Daemon,left,right
48
+ Diffusion CG,keep,keep
49
+ DiffusionCocktail,left,right
50
+ DyCFG,left,right
51
+ Dynamic Lora Weights,left,right
52
+ Dynamic Thresholding (CFG Scale Fix),left,right
53
+ Extra Samplers,left,right
54
+ FABRIC,left,right
55
+ First Block Cache,left,right
56
+ Forge Couple,left,right
57
+ FreeU,left,right
58
+ Global Prompts,left,right
59
+ HighRes fix: override CFG and Distilled CFG,left,right
60
+ Hires. Fix (txt2img + img2img),left,right
61
+ Image Filters,left,right
62
+ img2img Hires Fix,left,right
63
+ Incantations,left,right
64
+ Inpaint Mask Tools,keep,keep
65
+ Kohya Hires.fix,keep,keep
66
+ Large Initial Thresholding for Forge,left,right
67
+ Latent Couple,left,right
68
+ Latent Drift Correction,left,right
69
+ Latent Mirroring,left,right
70
+ Latent Playground,left,left
71
+ LoRA Block Weight,left,right
72
+ Lora Masks,left,right
73
+ Masa Control,keep,keep
74
+ Merge Options,left,right
75
+ Multiple Hypernetworks,left,right
76
+ NegPiP,left,right
77
+ Neutral Prompt,left,right
78
+ Noise Color Picker,left,left
79
+ Noise scheduler modifier,left,right
80
+ Overlay,left,right
81
+ Parameters info,left,right
82
+ Perturbed Attention Guidance,left,right
83
+ Photo Refiner 2.0,left,right
84
+ picBatchWork,left,right
85
+ Pick CLIP,left,right
86
+ Predefined Aspect Ratios,left,right
87
+ Prompt Refactor,left,right
88
+ Quick Recents,left,right
89
+ Ranbooru,left,right
90
+ Regional Prompter,left,right
91
+ ReSharpen,left,right
92
+ Samplers Scheduler Seniorious,left,right
93
+ ScaleCrafter,keep,keep
94
+ Scheduler Override,left,right
95
+ Scramble Prompts [M9],left,right
96
+ Scripts,keep,keep
97
+ SDXL Latent Tweaking,left,right
98
+ SDXL Resolution Picker,left,right
99
+ SDXL Resolutions,left,right
100
+ SDXL Styles,left,right
101
+ Seed Travel Extras...,left,right
102
+ Self Attention Guidance,left,right
103
+ Semantic Guidance,left,right
104
+ Skimmed CFG,left,right
105
+ Snakeoil CFG for reForge,left,right
106
+ Some Image Effects,left,right
107
+ SSaSD,left,right
108
+ Symmetry,keep,keep
109
+ TeaCache,left,right
110
+ Tiled Diffusion,left,right
111
+ Tiled VAE,left,right
112
+ Trip Clip Skip,left,right
113
+ Tweak Weights [M9],left,right
114
+ Txt2img → Img2img,left,right
115
+ Vectorscope CC,left,right
116
+ Workflow,left,right
117
+ XL Vec,left,right
118
+ Yet Another VAE Tiling,left,right
119
+ µ Detection Detailer,left,right
sd-webui-tabs-extension/ui.png ADDED