Upload sd-webui-tabs-extension using SD-Hub
Browse files- sd-webui-tabs-extension/.gitignore +2 -0
- sd-webui-tabs-extension/.ipynb_checkpoints/tab_configs-checkpoint.csv +119 -0
- sd-webui-tabs-extension/LICENSE +21 -0
- sd-webui-tabs-extension/README.md +71 -0
- sd-webui-tabs-extension/javascript/tabs_configs.js +108 -0
- sd-webui-tabs-extension/javascript/tabs_ex.js +314 -0
- sd-webui-tabs-extension/javascript/tabs_parser.js +176 -0
- sd-webui-tabs-extension/scripts/__pycache__/tabs_config_io.cpython-310.pyc +0 -0
- sd-webui-tabs-extension/scripts/__pycache__/tabs_settings.cpython-310.pyc +0 -0
- sd-webui-tabs-extension/scripts/tabs_config_io.py +120 -0
- sd-webui-tabs-extension/scripts/tabs_settings.py +141 -0
- sd-webui-tabs-extension/style.css +59 -0
- sd-webui-tabs-extension/tab_configs.csv +119 -0
- sd-webui-tabs-extension/ui.png +0 -0
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
|