├── .gitattributes
├── .gitignore
├── LICENSE
├── additional_style_files.md
├── changes.md
├── javascript
└── main.js
├── readme.md
├── scripts
├── additionals.py
├── background.py
├── filemanager.py
├── install.py
├── main.py
└── shared.py
└── todo.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | # pytype static type analyzer
142 | .pytype/
143 |
144 | # Cython debug symbols
145 | cython_debug/
146 |
147 | # PyCharm
148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150 | # and can be added to the global gitignore or merged into this file. For a more nuclear
151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152 | #.idea/
153 | scripts/Styles-Editor.code-workspace
154 | *.csv
155 | test.py
156 | lasthash.json
157 | backups/
158 | notes.json
159 | .vscode/settings.json
160 | .DS_Store
161 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/additional_style_files.md:
--------------------------------------------------------------------------------
1 | # Working with additional style files
2 | If you have a lot of styles you might find it useful to break them up into categories. This functionality is available through the `Edit additional style files` checkbox (under `Advanced`). These additional style files are stored (as `.csv`) in `extensions/Styles-Editor/additonal_style_files`.
3 |
4 | ## Basic idea
5 | Any style that is in an additional style file gets renamed to `filename::stylename`. So if you put a style `Picasso` into an additional style file `Artists`, it will now have the name `Artists::Picasso`.
6 |
7 | ## Creating and removing additional style files
8 | Tick the `Edit additional style files` box, then use `--Create New--` in the dropdown menu to create the categories you want. If there are no additional style files when you tick the box, you will automatically be prompted to create one.
9 |
10 | Alternatively, add the prefix `title::` to a style in the master view: the additional style file `title` will be created for you when you next tick the `Edit additional style files` box.
11 |
12 | Empty additional style files are removed when you uncheck the `Edit additional style files` box.
13 |
14 | ## Saving
15 | The additional style files are autosaved as you edit. They are merged into the master style file when you uncheck the `Edit additional style files` box, or when you move to a different tab.
16 |
17 | ## Moving styles to or between an additional style file
18 | Select style or styles (with right click) then press `M` and enter the new prefix.
19 |
20 |
--------------------------------------------------------------------------------
/changes.md:
--------------------------------------------------------------------------------
1 | # Recent changes
2 |
3 | ## 12 July 2023
4 | - Make mac command key work for cut, copy, paste
5 | - Fix delete of cell when no row selected
6 | - `D` to duplicate selected row(s)
7 |
8 | ## 9 July 2023
9 | - Allow backups to be downloaded
10 |
11 | ## 8 July 2023
12 | - Fixed some bugs
13 | - List existing backups to restore from
14 |
15 | ## 5 July 2023
16 | - Changes under the covers to increase performance
17 | - Automatically merge additional styles when moving to another tab
18 |
19 | ## 4 July 2023
20 | - Move styles to new additional style file
21 |
22 | ## 3 July 2023
23 | - Moved delete style functionality into an API call
24 | - Ctrl-right-click to select multiple rows
25 |
26 | ## 1 July 2023
27 | - Moved `create new additional style file` to the dropdown menu
28 | - Removed `merge into master` - now it happens automatically when `Edit additional` is unchecked
29 | - Fixed a crashing bug when a style had no name
30 | - Added a subtle color shading to indicate filter and encryption are active even when closed
31 | - Moved checkboxes to an `Advanced Options` accordian
32 |
33 | ## 30 June 2023
34 | - Delete from master list removes from additional style file as well
35 | - Major refactoring of code - sorry if I broke things, but it's going to be a lot easier going forward.
36 |
37 | ## 29 June 2023
38 | - Layout tweaks and minor fixes
39 |
40 | ## 28 June 2023
41 | - Allow linebreaks in styles (represented as `
` in editor)
42 | - Restore from backups
43 |
44 | ## 22 June 2023
45 | - Option to encrypt backups
46 |
47 | ## 21 June 2023
48 | - Automatically create new Additional Style Files if needed
49 | - Automatically delete empty Additional Style Files on merge
50 | - Added notes column back in
51 | - Fixed some minor bugs
52 |
53 | ## 20 June 2023
54 | - Regular backups created in `extensions/Styles-Editor/backups` (saved every ten minutes if changes have been made, last 12 retained)
55 | - Updated most of the documentation
56 | - Removed `Renumber Sort Column` button (just switch tabs and switch back!)
57 | - Removed `Extract from Master` button (automatically done when you go into additional style files view)
58 |
59 | ## 19 June 2023
60 | - Right-click can be used to select a row in the table (a style)
61 | - Delete the selected style by pressing `backspace`/`delete`
--------------------------------------------------------------------------------
/javascript/main.js:
--------------------------------------------------------------------------------
1 | function api_post(path, payload, callback) {
2 | var xhr = new XMLHttpRequest();
3 | xhr.open("POST", path, true);
4 | xhr.onload = function() { callback(JSON.parse(xhr.responseText)); };
5 | xhr.setRequestHeader("Content-type", "application/json");
6 | xhr.send(JSON.stringify(payload));
7 | }
8 |
9 | function when_loaded() {
10 | api_post("/style-editor/check-api/", {}, function(x) { console.log( "Style Editor Check API", x['value'] )});
11 | globalThis.selectedRows = [];
12 | grid = document.getElementById('style_editor_grid');
13 | grid.addEventListener('keydown', function(event){
14 | // if a key is pressed in a TD which has an INPUT child, or an INPUT, this is typing in a cell, allow it
15 | if (event.target.tagName === 'TD' && event.target.querySelector("input")) { return; }
16 | if (event.target.tagName === 'INPUT') { return; }
17 |
18 | if (event.ctrlKey === true || event.metaKey === true) {
19 | event.stopImmediatePropagation();
20 | span = event.target.querySelector("span");
21 | if (event.key === 'c') {
22 | navigator.clipboard.writeText(span.textContent);
23 | }
24 | if (event.key === 'x') {
25 | navigator.clipboard.writeText(span.textContent);
26 | update(event.target, "");
27 | }
28 | if (event.key === 'v') {
29 | navigator.clipboard.readText().then((clipText) => (update(event.target,clipText)));
30 | }
31 | }
32 |
33 | // if M is pressed, move the selected styles
34 | if (event.key === "m" && globalThis.selectedRows.length > 0) {
35 | new_prefix = prompt("Move to style file:", "");
36 | if (new_prefix != null) {
37 | globalThis.selectedRows.forEach( function(row) {
38 | api_post("/style-editor/move-style",
39 | {"style":{"value":row_style_name(row)}, "new_prefix":{"value":new_prefix}},
40 | function(x){} );
41 | });
42 | document.getElementById("style_editor_handle_api").click();
43 | unselect_rows();
44 | }
45 | }
46 |
47 | // if D is pressed, duplicate the selected styles
48 | if (event.key === "d" && globalThis.selectedRows.length > 0) {
49 | globalThis.selectedRows.forEach( function(row) {
50 | api_post("/style-editor/duplicate-style",
51 | {"value":row_style_name(row)},
52 | function(x){} );
53 | });
54 | document.getElementById("style_editor_handle_api").click();
55 | unselect_rows();
56 | }
57 |
58 | // if backspace or delete are pressed, delete selected rows
59 | if (event.key === "Backspace" || event.key === "Delete") {
60 | if (globalThis.selectedRows.length > 0) {
61 | globalThis.selectedRows.forEach( function(row) {
62 | api_post("/style-editor/delete-style",
63 | {"value":row_style_name(row)},
64 | function(x){} );
65 | });
66 | document.getElementById("style_editor_handle_api").click();
67 | } else {
68 | update(event.target,"");
69 | }
70 | globalThis.selectedRows = [];
71 | }
72 |
73 | // if we get to here, stop the keypress from propogating
74 | event.stopImmediatePropagation();
75 | }, { capture: true });
76 |
77 | grid.addEventListener('contextmenu', function(event){
78 | if(event.shiftKey) { return; }
79 | row = event.target.closest("tr");
80 | if (row) { select_row(row); event.stopImmediatePropagation(); event.preventDefault(); }
81 | }, { capture: true });
82 |
83 | grid.addEventListener('click', function(event){
84 | if (event.ctrlKey || event.metaKey) {
85 | row = event.target.closest("tr");
86 | if (row) { select_row(row); event.stopImmediatePropagation(); event.preventDefault(); }
87 | } else {
88 | unselect_rows();
89 | }
90 | }, { capture: true });
91 | }
92 |
93 | function row_style_name(row) {
94 | return row.querySelectorAll("td")[1].querySelector("span").textContent;
95 | }
96 |
97 | function select_row(row) {
98 | globalThis.selectedRows.push(row);
99 | row.style.backgroundColor = "#840";
100 | }
101 |
102 | function unselect_rows() {
103 | globalThis.selectedRows.forEach( function(row){ row.style.backgroundColor = ''; })
104 | globalThis.selectedRows = []
105 | }
106 |
107 | function press_refresh_button(tab) {
108 | b = document.getElementById("refresh_txt2img_styles");
109 | if (b) {b.click()}
110 | b = document.getElementById("refresh_img2img_styles");
111 | if (b) {b.click()}
112 | }
113 |
114 | function update(target, text) {
115 | // Update the cell in such a way as to get the backend to notice...
116 | // - generate a double click on the original target
117 | // - wait 10ms to make sure it has happened, then:
118 | // - paste the text into the input that has been created
119 | // - send a 'return' keydown event through the input
120 | const dblclk = new MouseEvent("dblclick", {"bubbles":true, "cancelable":true});
121 | target.dispatchEvent(dblclk);
122 | setTimeout( function() {
123 | const the_input = target.querySelector('input');
124 | the_input.value = text;
125 | const rtrn = new KeyboardEvent( "keydown", {
126 | 'key': 'Enter', 'target': the_input,
127 | 'view': window, 'bubbles': true, 'cancelable': true
128 | });
129 | the_input.dispatchEvent(rtrn);
130 | }, 10);
131 | }
132 |
133 | function encryption_change(value) {
134 | accordian_style = document.getElementById('style_editor_encryption_accordian').style;
135 | if (value) {
136 | accordian_style.color = "#f88";
137 | } else {
138 | accordian_style.color = "white";
139 | }
140 | return value
141 | }
142 |
143 | function filter_style_list(filter_text, type) {
144 | if (type=="regex") {
145 | filter = document.getElementById('style_editor_filter').firstElementChild.lastElementChild;
146 | try {
147 | re = new RegExp(filter_text);
148 | filter.style.color="white";
149 | }
150 | catch (error) {
151 | re = new RegExp();
152 | filter.style.color="red";
153 | }
154 | }
155 | accordian_style = document.getElementById('style_editor_filter_accordian').style;
156 | if (filter_text==="") {
157 | accordian_style.color = "white";
158 | } else {
159 | accordian_style.color = "#f88";
160 | }
161 | rows = document.getElementById('style_editor_grid').querySelectorAll("tr");
162 | header = true;
163 | for (row of rows) {
164 | vis = false;
165 | for (cell of row.querySelectorAll("span")) {
166 | if ( (type=="Exact match" && cell.textContent.includes(filter_text)) ||
167 | (type=="Case insensitive" && cell.textContent.toLowerCase().includes(filter_text.toLowerCase())) ||
168 | (type=="regex" && cell.textContent.match(re)) )
169 | { vis = true; };
170 | }
171 | if (vis || header) { row.style.display = '' } else { row.style.display='none' }
172 | header = false;
173 | }
174 | return [filter_text, type]
175 | }
176 |
177 | function style_file_selection_change(x,y) {
178 | if (x==='--Create New--') {
179 | return [new_style_file_dialog(''),'']
180 | }
181 | return [x,'']
182 | }
183 |
184 | function new_style_file_dialog(x) {
185 | let filename = prompt("New style filename", "");
186 | if (filename == null) { filename = "" }
187 | return filename;
188 | }
189 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Style Editor
2 |
3 | *Note that due to me moving across to ComfyUI this repository is no longer being maintained*
4 |
5 | An extension for Automatic1111 to add a Style Editor, which allows you to view and edit saved styles in a spreadsheet-like format.
6 |
7 | See also:
8 | - [Recent Changes](./changes.md "Recent Changes")
9 | - [To Do](/todo.md "To Do")
10 | - [Additional Style Files](/additional_style_files.md "Working with additional style files")
11 |
12 | ## Installation
13 |
14 | In Automatic1111 you can add this extension through the extensions index.
15 |
16 | Alternatively, paste the url `https://github.com/chrisgoringe/Styles-Editor` into the manual install URL box.
17 |
18 | Or clone the repository into your extensions folder.
19 |
20 | ## Gradio
21 | This extension requires `gradio 3.30` or above. Automatic1111 moved to this on May 19th 2023, so if you have updated since then you'll be fine.
22 |
23 | If you get an error `AttributeError: 'Dataframe' object has no attribute 'input'`, then you are probably still running `gradio 3.29`. Check before raising an issue!
24 |
25 | ## Basic Usage
26 |
27 | ### Edit styles
28 | Double-click in any of the boxes to get an edit cursor within the box.
29 |
30 | ### Search and replace
31 | Enter a search term and a replace term and press the button...
32 |
33 | ### Cut, copy, paste
34 | Click on a cell to select it, then use Ctrl-X, C and V.
35 |
36 | ### Delete styles
37 | Right-click on a style to select that row. Then hit `backspace` or `delete`. You can select multiple rows by ctrl-clicking.
38 |
39 | ### Add styles
40 | Use the `New row` button, and then edit the boxes as you need. Or select a row and press `D` to duplicate it.
41 | Note that if you have a filter applied the new row probably won't appear because it is empty, so best not to do that.
42 |
43 | ### Duplicate style names
44 | The editor will not allow styles to have the same name, so one or more 'x's will be appended to any duplicates.
45 |
46 | ### Save styles
47 | Styles are saved automatically.
48 |
49 | ### Filter view
50 | Type into the filter text box to only show rows matching the text string. Matches from any of the columns. Filter can be set to Exact match, case insensitive, or regex.
51 | If filtering by regex, if an invalid regex is entered it will be highlighted in red.
52 |
53 | ### Sorting
54 | The `sort` column is automatically generated whenever you save or load. If you select `autosort` (under `Advanced`) the table will automatically sort whenever you change any `sort` value (as long as every `sort` value is numeric).
55 |
56 | ### Notes
57 | You can put whatever you want in the notes column.
58 |
59 | ### Encryption
60 | Check the `Use encryption` box and all (subsequent) backups will be encrypted using the key you specify.
61 | Encryption is done using [pyAesCrypt](https://pypi.org/project/pyAesCrypt/).
62 |
63 | ### Backups
64 | The master style file is backed up every ten minutes (if changes have been made), with the most recent 24 backups retained. Backups are stored in `extensions/Styles-Editor/backups`.
65 |
66 | To restore a backup, drag and drop the backup style file into the `restore from backup` box, or select one of the backups from the dropdown (the names are date_time in format `YYMMDD_HHMM`). If it is encrypted (`.aes`) then the encryption key in the `Encryption` section is used to decrypt.
67 |
68 | To download a backup, select it from the dropdown then click the `download` link that appears in the upload/download box.
69 |
70 | ### Stargazers
71 | Thanks to those who've starred this - knowing people value the extension makes it worth working on.
72 | - 20 on 21 June 2023
73 | - 31 on 6 July 2023
74 |
--------------------------------------------------------------------------------
/scripts/additionals.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | class Additionals:
4 | @classmethod
5 | def init(cls, default_style_file_path, additional_style_files_directory) -> None:
6 | cls.default_style_file_path = default_style_file_path
7 | cls.additional_style_files_directory = additional_style_files_directory
8 |
9 | @staticmethod
10 | def has_prefix(fullname:str):
11 | """
12 | Return true if the fullname is prefixed.
13 | """
14 | return ('::' in fullname)
15 |
16 | @staticmethod
17 | def split_stylename(fullname:str):
18 | """
19 | Split a stylename in the form [prefix::]name into prefix, name or None, name
20 | """
21 | if '::' in fullname:
22 | return fullname[:fullname.find('::')], fullname[fullname.find('::')+2:]
23 | else:
24 | return None, fullname
25 |
26 | @staticmethod
27 | def merge_name(prefix:str, name:str):
28 | """
29 | Merge prefix and name into prefix::name (or name, if prefix is none or '')
30 | """
31 | if prefix:
32 | return prefix+"::"+name
33 | else:
34 | return name
35 |
36 | @staticmethod
37 | def prefixed_style(maybe_prefixed_style: str, current_prefix:str, force=False):
38 | """
39 | If force is False:
40 | If it has a prefix, return it.
41 | If not, use the one specified. If that is None or '', no prefix
42 | If force is True:
43 | use the prefix specified
44 | """
45 | prefix, style = Additionals.split_stylename(maybe_prefixed_style)
46 | prefix = current_prefix if force else (prefix or current_prefix)
47 | return Additionals.merge_name(prefix, style)
48 |
49 | @classmethod
50 | def full_path(cls, filename:str) -> str:
51 | """
52 | Return the full path for an additional style file.
53 | Input can be the full path, the filename with extension, or the filename without extension.
54 | If input is None, '', or the default style file path, return the default style file path
55 | """
56 | if filename is None or filename=='' or filename==cls.default_style_file_path:
57 | return cls.default_style_file_path
58 | filename = filename+".csv" if not filename.endswith(".csv") else filename
59 | return os.path.relpath(os.path.join(cls.additional_style_files_directory,os.path.split(filename)[1]))
60 |
61 | @classmethod
62 | def display_name(cls, filename:str) -> str:
63 | """
64 | Return the full path for an additional style file.
65 | Input can be the full path, the filename with extension, or the filename without extension
66 | """
67 | fullpath = cls.full_path(filename)
68 | return os.path.splitext(os.path.split(fullpath)[1])[0] if fullpath!=cls.default_style_file_path else ''
69 |
70 | @classmethod
71 | def additional_style_files(cls, include_new, display_names):
72 | format = cls.display_name if display_names else cls.full_path
73 | additional_style_files = [format(f) for f in os.listdir(cls.additional_style_files_directory) if f.endswith(".csv")]
74 | return additional_style_files+["--Create New--"] if include_new else additional_style_files
75 |
76 | @classmethod
77 | def prefixes(cls):
78 | return cls.additional_style_files(include_new=False, display_names=True)
--------------------------------------------------------------------------------
/scripts/background.py:
--------------------------------------------------------------------------------
1 | import time, threading
2 |
3 | class Background:
4 | """
5 | A simple background task manager that considers doing a background task every n seconds.
6 | The task is only done if the manager has been marked as pending.
7 | """
8 | def __init__(self, method, sleeptime) -> None:
9 | """
10 | Create a manager that will consider calling `method` every `sleeptime` seconds
11 | """
12 | self.method = method
13 | self.sleeptime = sleeptime
14 | self._pending = False
15 | self._started = False
16 | self.lock = threading.Lock()
17 |
18 | def start(self):
19 | """
20 | Start the manager's thread
21 | """
22 | with self.lock:
23 | if not self._started:
24 | threading.Thread(group=None, target=self._action, daemon=True).start()
25 | self._started = True
26 |
27 | def set_pending(self, pending=True):
28 | """
29 | Set the task as pending. Next time the manager checks it will call `method` and then unset pending.
30 | """
31 | with self.lock:
32 | self._pending = pending
33 |
34 | def _action(self):
35 | while True:
36 | with self.lock:
37 | if self._pending:
38 | self.method()
39 | self._pending = False
40 | time.sleep(self.sleeptime)
41 |
--------------------------------------------------------------------------------
/scripts/filemanager.py:
--------------------------------------------------------------------------------
1 | # A bunch of utility methods to load and save style files
2 | import pandas as pd
3 | import numpy as np
4 | import os, json
5 | from typing import Dict
6 | from modules.shared import cmd_opts, opts, prompt_styles
7 | import modules.scripts as scripts
8 | import shutil
9 | import datetime
10 | from pathlib import Path
11 | try:
12 | import pyAesCrypt
13 | except:
14 | print("No pyAesCrypt - won't be able to do encryption")
15 | from scripts.additionals import Additionals
16 | from scripts.shared import columns, user_columns, display_columns, d_types, name_column
17 |
18 | class StyleFile:
19 | def __init__(self, prefix:str):
20 | self.prefix = prefix
21 | self.filename = Additionals.full_path(prefix)
22 | self.data:pd.DataFrame = self._load()
23 |
24 | def _load(self):
25 | try:
26 | data = pd.read_csv(self.filename, header=None, names=columns,
27 | encoding="utf-8-sig", dtype=d_types,
28 | skiprows=[0], usecols=[0,1,2])
29 | except:
30 | data = pd.DataFrame(columns=columns)
31 |
32 | indices = range(data.shape[0])
33 | data.insert(loc=0, column="sort", value=[i+1 for i in indices])
34 | data.fillna('', inplace=True)
35 | data.insert(loc=4, column="notes",
36 | value=[FileManager.lookup_notes(data['name'][i], self.prefix) for i in indices])
37 | if len(data)>0:
38 | for column in user_columns:
39 | data[column] = data[column].str.replace('\n', '
',regex=False)
40 | return data
41 |
42 | @staticmethod
43 | def sort_dataset(data:pd.DataFrame) -> pd.DataFrame:
44 | def _to_numeric(series:pd.Series):
45 | nums = pd.to_numeric(series)
46 | if any(nums.isna()):
47 | raise Exception("don't update display")
48 | return nums
49 |
50 | try:
51 | return data.sort_values(by='sort', axis='index', inplace=False, na_position='first', key=_to_numeric)
52 | except:
53 | return data
54 |
55 | def save(self):
56 | self.fix_duplicates()
57 | clone = self.data.copy()
58 | if len(clone)>0:
59 | for column in user_columns:
60 | clone[column] = clone[column].str.replace('
', '\n',regex=False)
61 | clone.to_csv(self.filename, encoding="utf-8-sig", columns=columns, index=False)
62 |
63 | def fix_duplicates(self):
64 | names = self.data['name']
65 | used = set()
66 | for index, value in names.items():
67 | if value in used:
68 | while value in used:
69 | value = value + "x"
70 | names.at[index] = value
71 | used.add(value)
72 |
73 | class FileManager:
74 | basedir = scripts.basedir()
75 | additional_style_files_directory = os.path.join(basedir,"additonal_style_files")
76 | backup_directory = os.path.join(basedir,"backups")
77 | if not os.path.exists(backup_directory):
78 | os.mkdir(backup_directory)
79 | if not os.path.exists(additional_style_files_directory):
80 | os.mkdir(additional_style_files_directory)
81 |
82 | try:
83 | default_style_file_path = cmd_opts.styles_file
84 | except:
85 | default_style_file_path = getattr(opts, 'styles_dir', None)
86 | current_styles_file_path = default_style_file_path
87 |
88 | Additionals.init(default_style_file_path=default_style_file_path, additional_style_files_directory=additional_style_files_directory)
89 |
90 | try:
91 | with open(os.path.join(basedir, "notes.json")) as f:
92 | notes_dictionary = json.load(f)
93 | except:
94 | notes_dictionary = {}
95 |
96 | encrypt = False
97 | encrypt_key = ""
98 | loaded_styles:Dict[str,StyleFile] = {}
99 |
100 | @classmethod
101 | def clear_style_cache(cls):
102 | """
103 | Drop all loaded styles
104 | """
105 | cls.loaded_styles = {}
106 |
107 | @classmethod
108 | def get_current_styles(cls):
109 | return cls.get_styles(cls._current_prefix())
110 |
111 | @classmethod
112 | def using_additional(cls):
113 | return cls._current_prefix()!=''
114 |
115 | @classmethod
116 | def get_styles(cls, prefix='') -> pd.DataFrame:
117 | """
118 | If prefix is '', this is the default style file.
119 | Load or retrieve from cache
120 | """
121 | if not prefix in cls.loaded_styles:
122 | cls.loaded_styles[prefix] = StyleFile(prefix)
123 | return cls.loaded_styles[prefix].data.copy()
124 |
125 | @classmethod
126 | def save_current_styles(cls, data):
127 | cls.save_styles(data, cls._current_prefix())
128 |
129 | @classmethod
130 | def save_styles(cls, data:pd.DataFrame, prefix=''):
131 | if not prefix in cls.loaded_styles:
132 | cls.loaded_styles[prefix] = StyleFile(prefix)
133 | cls.loaded_styles[prefix].data = data
134 | cls.loaded_styles[prefix].save()
135 |
136 | cls.update_notes_dictionary(data, prefix)
137 | cls.save_notes_dictionary()
138 | prompt_styles.reload()
139 |
140 | @staticmethod
141 | def create_file_if_missing(filename):
142 | filename = Additionals.full_path(filename)
143 | if not os.path.exists(filename):
144 | print("", file=open(filename,"w"))
145 |
146 | @staticmethod
147 | def add_or_replace(array:np.ndarray, row):
148 | for i in range(len(array)):
149 | if array[i][1] == row.iloc[1]:
150 | array[i] = row
151 | return array
152 | return np.vstack([array,row])
153 |
154 | @classmethod
155 | def update_additional_style_files(cls):
156 | additional_files_as_numpy = { prefix : FileManager.get_styles(prefix=prefix).to_numpy() for prefix in Additionals.additional_style_files(include_new=False, display_names=True) }
157 | for _, row in cls.get_styles().iterrows():
158 | prefix, row.iloc[1] = Additionals.split_stylename(row.iloc[1])
159 | if prefix:
160 | if prefix in additional_files_as_numpy:
161 | additional_files_as_numpy[prefix] = cls.add_or_replace(additional_files_as_numpy[prefix], row)
162 | else:
163 | additional_files_as_numpy[prefix] = np.vstack([row])
164 | for prefix in additional_files_as_numpy:
165 | cls.save_styles(pd.DataFrame(additional_files_as_numpy[prefix], columns=display_columns), prefix=prefix)
166 |
167 | @classmethod
168 | def merge_additional_style_files(cls):
169 | styles = cls.get_styles('')
170 | styles = styles.drop(index=[i for (i, row) in styles.iterrows() if Additionals.has_prefix(row.iloc[1])])
171 | for prefix in Additionals.prefixes():
172 | styles_with_prefix = cls.get_styles(prefix=prefix).copy()
173 | if len(styles_with_prefix)==0:
174 | os.remove(Additionals.full_path(prefix))
175 | else:
176 | styles_with_prefix[name_column] = [Additionals.merge_name(prefix,x) for x in styles_with_prefix[name_column]]
177 | styles = pd.concat([styles, styles_with_prefix], ignore_index=True)
178 | styles['sort'] = [i+1 for i in range(len(styles['sort']))]
179 | cls.save_styles(styles)
180 |
181 | @classmethod
182 | def _current_prefix(cls):
183 | return Additionals.display_name(cls.current_styles_file_path)
184 |
185 | @classmethod
186 | def move_to_additional(cls, maybe_prefixed_style, new_prefix):
187 | old_prefixed_style = Additionals.prefixed_style(maybe_prefixed_style, cls._current_prefix())
188 | new_prefixed_style = Additionals.prefixed_style(maybe_prefixed_style, new_prefix, force=True)
189 | data = cls.get_styles()
190 | data[name_column] = data[name_column].str.replace(old_prefixed_style, new_prefixed_style)
191 | cls.save_styles(data)
192 | cls.remove_from_additional(old_prefixed_style)
193 | cls.update_additional_style_files()
194 |
195 | @classmethod
196 | def remove_style(cls, maybe_prefixed_style):
197 | prefixed_style = Additionals.prefixed_style(maybe_prefixed_style, cls._current_prefix())
198 | data = cls.get_styles()
199 | rows_to_drop = [i for (i, row) in data.iterrows() if row.iloc[1]==prefixed_style]
200 | cls.save_styles(data.drop(index=rows_to_drop))
201 | cls.remove_from_additional(prefixed_style)
202 | cls.update_additional_style_files()
203 |
204 | @classmethod
205 | def duplicate_style(cls, maybe_prefixed_style):
206 | prefixed_style = Additionals.prefixed_style(maybe_prefixed_style, cls._current_prefix())
207 | data = cls.get_styles()
208 | new_rows = pd.DataFrame([row for (i, row) in data.iterrows() if row.iloc[1]==prefixed_style])
209 | data = pd.concat([data, new_rows], ignore_index=True)
210 | data = StyleFile.sort_dataset(data)
211 | cls.save_styles(data)
212 | cls.update_additional_style_files()
213 |
214 | @classmethod
215 | def remove_from_additional(cls, maybe_prefixed_style):
216 | prefix, style = Additionals.split_stylename(maybe_prefixed_style)
217 | if prefix:
218 | data = cls.get_styles(prefix)
219 | data = data.drop(index=[i for (i, row) in data.iterrows() if row.iloc[1]==style])
220 | cls.save_styles(data, prefix=prefix)
221 |
222 | @classmethod
223 | def do_backup(cls):
224 | fileroot = os.path.join(cls.backup_directory, datetime.datetime.now().strftime("%y%m%d_%H%M"))
225 | if not os.path.exists(cls.default_style_file_path):
226 | return
227 | shutil.copyfile(cls.default_style_file_path, fileroot+".csv")
228 | paths = sorted(Path(cls.backup_directory).iterdir(), key=os.path.getmtime, reverse=True)
229 | for path in paths[24:]:
230 | os.remove(str(path))
231 | if cls.encrypt and len(cls.encrypt_key)>0:
232 | try:
233 | pyAesCrypt.encryptFile(fileroot+".csv", fileroot+".csv.aes", cls.encrypt_key)
234 | os.remove(fileroot+".csv")
235 | except:
236 | print("Failed to encrypt")
237 |
238 | @classmethod
239 | def list_backups(cls):
240 | return [file for file in os.listdir(cls.backup_directory) if (file.endswith('csv') or file.endswith('aes'))]
241 |
242 | @classmethod
243 | def backup_file_path(cls, file):
244 | return os.path.join(cls.backup_directory, file)
245 |
246 | @classmethod
247 | def restore_from_backup(cls, file):
248 | path = cls.backup_file_path(file)
249 | if not os.path.exists(path):
250 | return "Invalid selection"
251 | if os.path.splitext(file)[1]==".aes":
252 | try:
253 | temp = os.path.join(cls.backup_directory, "temp.aes")
254 | temd = os.path.join(cls.backup_directory, "temp.csv")
255 | shutil.copyfile(file,temp)
256 | pyAesCrypt.decryptFile(temp, temd, cls.encrypt_key)
257 | os.rename(temd, cls.default_style_file_path)
258 | except:
259 | error = "Failed to decrypt .aes file"
260 | finally:
261 | if os.path.exists(temp):
262 | os.remove(temp)
263 | if os.path.exists(temd):
264 | os.remove(temd)
265 | else:
266 | shutil.copyfile(path, cls.default_style_file_path)
267 | return None
268 |
269 |
270 | @classmethod
271 | def restore_from_upload(cls, tempfile):
272 | error = None
273 | if os.path.exists(cls.default_style_file_path):
274 | if os.path.exists(cls.default_style_file_path+".temp"):
275 | os.remove(cls.default_style_file_path+".temp")
276 | os.rename(cls.default_style_file_path, cls.default_style_file_path+".temp")
277 | if os.path.splitext(tempfile)[1]==".aes":
278 | try:
279 | pyAesCrypt.decryptFile(tempfile, cls.default_style_file_path, cls.encrypt_key)
280 | except:
281 | error = "Failed to decrypt .aes file"
282 | elif os.path.splitext(tempfile)[1]==".csv":
283 | os.rename(tempfile, cls.default_style_file_path)
284 | else:
285 | error = "Can only restore from .csv or .aes file"
286 | if os.path.exists(cls.default_style_file_path+".temp"):
287 | if os.path.exists(cls.default_style_file_path):
288 | os.remove(cls.default_style_file_path+".temp")
289 | else:
290 | os.rename(cls.default_style_file_path+".temp", cls.default_style_file_path)
291 | return error
292 |
293 | @classmethod
294 | def save_notes_dictionary(cls):
295 | print(json.dumps(cls.notes_dictionary),file=open(os.path.join(cls.basedir, "notes.json"), 'w'))
296 |
297 | @classmethod
298 | def update_notes_dictionary(cls, data:pd.DataFrame, prefix:str):
299 | for _, row in data.iterrows():
300 | stylename = prefix+"::"+row.iloc[1] if prefix!='' else row.iloc[1]
301 | cls.notes_dictionary[stylename] = row.iloc[4]
302 |
303 | @classmethod
304 | def lookup_notes(cls, stylename, prefix):
305 | stylename = prefix+"::"+stylename if prefix!='' else stylename
306 | return cls.notes_dictionary[stylename] if stylename in cls.notes_dictionary else ''
--------------------------------------------------------------------------------
/scripts/install.py:
--------------------------------------------------------------------------------
1 | import launch
2 |
3 | if not launch.is_installed("pyAesCrypt"):
4 | launch.run_pip("install pyAesCrypt", "pyAesCrypt for Styles Editor")
--------------------------------------------------------------------------------
/scripts/main.py:
--------------------------------------------------------------------------------
1 | import gradio as gr
2 | from fastapi import FastAPI, Request, status
3 | from fastapi.exceptions import RequestValidationError
4 | from fastapi.responses import JSONResponse
5 | from pydantic import BaseModel
6 | from typing import Tuple
7 | import modules.scripts as scripts
8 | from modules import script_callbacks
9 |
10 | import pandas as pd
11 | import threading
12 |
13 | from scripts.filemanager import FileManager, StyleFile
14 | from scripts.additionals import Additionals
15 | from scripts.background import Background
16 | from scripts.shared import display_columns
17 |
18 | class Script(scripts.Script):
19 | def __init__(self) -> None:
20 | super().__init__()
21 |
22 | def title(self):
23 | return "Style Editor"
24 |
25 | def show(self, is_img2img):
26 | return scripts.AlwaysVisible
27 |
28 | def ui(self, is_img2img):
29 | return ()
30 |
31 | class ParameterString(BaseModel):
32 | value: str
33 |
34 | class ParameterBool(BaseModel):
35 | value: bool
36 |
37 | class StyleEditor:
38 | update_help = """# Recent changes:
39 | ## Changed in this update:
40 | - Make mac command key work for cut, copy, paste
41 | - Fix delete of cell when no row selected
42 | - Cmd / Ctrl / Right -click all select multiple rows
43 | - `D` to duplicate selected row
44 |
45 | ## Changed in recent updates:
46 | - Allow backups to be downloaded
47 | - Show backups in the `restore from backup` section
48 | - Automatically merge styles when changing away from this tab
49 | - Select row(s) then press `M` to move them
50 | - Ctrl-right-click to select multiple rows
51 |
52 | """
53 | brief_guide = """Click to select cell. Dbl-click to edit cell. Cmd-, Ctrl-, ⌘- or right-click to select rows.
54 |
55 | `Backspace/Delete` to clear selected cell/delete row(s). Ctrl- or ⌘- `X` `C` `V` cut copy or paste selected cell (not row).
56 | `M` to move selected row(s). `D` to duplicate selected row(s)"""
57 | backup = Background(FileManager.do_backup, 600)
58 | api_calls_outstanding = []
59 | api_lock = threading.Lock()
60 | this_tab_selected = False
61 |
62 | @classmethod
63 | def handle_this_tab_selected(cls):
64 | FileManager.clear_style_cache()
65 | FileManager.update_additional_style_files()
66 | cls.this_tab_selected = True
67 | return FileManager.get_current_styles()
68 |
69 | @classmethod
70 | def handle_another_tab_selected(cls):
71 | if cls.this_tab_selected:
72 | FileManager.merge_additional_style_files()
73 | FileManager.clear_style_cache()
74 | cls.this_tab_selected = False
75 |
76 | @classmethod
77 | def handle_autosort_checkbox_change(cls, data:pd.DataFrame, autosort) -> pd.DataFrame:
78 | if autosort:
79 | data = StyleFile.sort_dataset(data)
80 | FileManager.save_current_styles(data)
81 | return data
82 |
83 | @classmethod
84 | def handle_dataeditor_input(cls, data:pd.DataFrame, autosort) -> pd.DataFrame:
85 | cls.backup.set_pending()
86 | data = StyleFile.sort_dataset(data) if autosort else data
87 | FileManager.save_current_styles(data)
88 | return data
89 |
90 | @classmethod
91 | def handle_search_and_replace_click(cls, search:str, replace:str, current_data:pd.DataFrame):
92 | if len(search)==0:
93 | return current_data
94 | data_np = current_data.to_numpy()
95 | for i, row in enumerate(data_np):
96 | for j, item in enumerate(row):
97 | if isinstance(item,str) and search in item:
98 | data_np[i][j] = item.replace(search, replace)
99 | return pd.DataFrame(data=data_np, columns=display_columns)
100 |
101 | @classmethod
102 | def handle_use_additional_styles_box_change(cls, activate, filename):
103 | FileManager.current_styles_file_path = Additionals.full_path(filename) if activate else FileManager.default_style_file_path
104 | if activate:
105 | FileManager.update_additional_style_files()
106 | labels = Additionals.additional_style_files(display_names=True, include_new=True)
107 | selected = Additionals.display_name(FileManager.current_styles_file_path)
108 | selected = selected if selected in labels else labels[0] if len(labels)>0 else ''
109 | return gr.Row.update(visible=activate), FileManager.get_current_styles(), gr.Dropdown.update(choices=labels, value=selected)
110 | else:
111 | FileManager.merge_additional_style_files()
112 | return gr.Row.update(visible=activate), FileManager.get_current_styles(), gr.Dropdown.update()
113 |
114 | @classmethod
115 | def handle_style_file_selection_change(cls, prefix, _):
116 | if prefix:
117 | FileManager.create_file_if_missing(prefix)
118 | FileManager.current_styles_file_path = Additionals.full_path(prefix)
119 | else:
120 | prefix = Additionals.display_name(FileManager.current_styles_file_path)
121 | return FileManager.get_current_styles(), gr.Dropdown.update(choices=Additionals.additional_style_files(display_names=True, include_new=True), value=prefix)
122 |
123 | @classmethod
124 | def handle_use_encryption_checkbox_changed(cls, encrypt):
125 | FileManager.encrypt = encrypt
126 | return ""
127 |
128 | @classmethod
129 | def handle_encryption_key_change(cls, key):
130 | FileManager.encrypt_key = key
131 |
132 | @classmethod
133 | def handle_restore_backup_file_upload(cls, tempfile):
134 | return cls._after_backup_restore( FileManager.restore_from_upload(tempfile) )
135 |
136 | @classmethod
137 | def handle_backup_restore_button_click(cls, selection):
138 | return cls._after_backup_restore( FileManager.restore_from_backup(selection) )
139 |
140 | @classmethod
141 | def _after_backup_restore(cls, error):
142 | if error is None:
143 | FileManager.clear_style_cache()
144 | FileManager.update_additional_style_files()
145 | return gr.Text.update(visible=True, value="Styles restored"), False, FileManager.get_styles()
146 | else:
147 | return gr.Text.update(visible=True, value=error), False, FileManager.get_styles()
148 |
149 | @classmethod
150 | def handle_restore_backup_file_clear(cls):
151 | return gr.Text.update(visible=False)
152 |
153 | @classmethod
154 | def handle_backup_selection_change(cls, selection):
155 | if selection=="Refresh list" or selection=="---":
156 | return gr.Dropdown.update(choices=FileManager.list_backups()+["---","Refresh list"], value="---"), gr.File.update()
157 | else:
158 | return gr.Dropdown.update(choices=FileManager.list_backups()+["---","Refresh list"], value=selection), gr.File.update(value=FileManager.backup_file_path(selection))
159 |
160 | @classmethod
161 | def handle_outstanding_api_calls(cls):
162 | with cls.api_lock:
163 | for command, value in cls.api_calls_outstanding:
164 | match command:
165 | case "delete":
166 | FileManager.remove_style(maybe_prefixed_style=value)
167 | case "move":
168 | FileManager.move_to_additional(maybe_prefixed_style=value[0], new_prefix=value[1])
169 | case "duplicate":
170 | FileManager.duplicate_style(maybe_prefixed_style=value)
171 | cls.api_calls_outstanding = []
172 | return FileManager.get_current_styles()
173 |
174 | @classmethod
175 | def on_ui_tabs(cls):
176 | with gr.Blocks(analytics_enabled=False) as style_editor:
177 | dummy_component = gr.Label(visible=False)
178 | with gr.Row():
179 | cls.do_api = gr.Button(visible=False, elem_id="style_editor_handle_api")
180 | with gr.Column(scale=1, min_width=400):
181 | with gr.Accordion(label="Documentation and Recent Changes", open=False):
182 | gr.HTML(value="Link to Documentation")
183 | gr.Markdown(value=cls.update_help)
184 | gr.HTML(value="Change log")
185 | with gr.Column(scale=1, min_width=400):
186 | with gr.Accordion(label="Encryption", open=False, elem_id="style_editor_encryption_accordian"):
187 | cls.use_encryption_checkbox = gr.Checkbox(value=False, label="Use Encryption")
188 | cls.encryption_key_textbox = gr.Textbox(max_lines=1, placeholder="encryption key", label="Encryption Key")
189 | gr.Markdown(value="If checked, and a key is provided, backups are encrypted. The active style file and additional style files are not.")
190 | gr.Markdown(value="Files are encrypted using pyAesCrypt (https://pypi.org/project/pyAesCrypt/)")
191 | with gr.Column(scale=1, min_width=400):
192 | with gr.Accordion(label="Restore/Download backups", open=False):
193 | gr.Markdown(value="If restoring from an encrypted backup, enter the encrption key under `Encryption` first.")
194 | gr.Markdown(value="To restore: select a backup from the dropdown and press `Restore`, or upload a `.csv` or `.aes` file below.")
195 | gr.Markdown(value="To download: select a backup from the dropdown then download it from the box below.")
196 | with gr.Row():
197 | cls.backup_selection = gr.Dropdown(choices=FileManager.list_backups()+["---","Refresh list"],value="---", label="Backups")
198 | cls.backup_restore_button = gr.Button(value="Restore")
199 | cls.restore_backup_file_upload = gr.File(file_types=[".csv", ".aes"], label="Upload / Download")
200 | cls.restore_result = gr.Text(visible=False, label="Result:")
201 | with gr.Column(scale=1, min_width=400):
202 | with gr.Accordion(label="Filter view", open=False, elem_id="style_editor_filter_accordian"):
203 | cls.filter_textbox = gr.Textbox(max_lines=1, interactive=True, placeholder="filter", elem_id="style_editor_filter", show_label=False)
204 | cls.filter_select = gr.Dropdown(choices=["Exact match", "Case insensitive", "regex"], value="Exact match", show_label=False)
205 | with gr.Column(scale=1, min_width=400):
206 | with gr.Accordion(label="Search and replace", open=False):
207 | cls.search_box = gr.Textbox(max_lines=1, interactive=True, placeholder="search for", show_label=False)
208 | cls.replace_box= gr.Textbox(max_lines=1, interactive=True, placeholder="replace with", show_label=False)
209 | cls.search_and_replace_button = gr.Button(value="Search and Replace")
210 | with gr.Column(scale=1, min_width=400):
211 | with gr.Accordion(label="Advanced options", open=False):
212 | cls.use_additional_styles_checkbox = gr.Checkbox(value=FileManager.using_additional(), label="Edit additional style files")
213 | cls.autosort_checkbox = gr.Checkbox(value=False, label="Autosort")
214 | with gr.Group(visible=False) as cls.additional_file_display:
215 | cls.style_file_selection = gr.Dropdown(choices=Additionals.additional_style_files(display_names=True, include_new=True),
216 | value=Additionals.display_name(''),
217 | label="Additional Style File", scale=1, min_width=200)
218 | with gr.Row():
219 | gr.Markdown(cls.brief_guide)
220 | with gr.Row():
221 | cls.dataeditor = gr.Dataframe(value=FileManager.get_current_styles(), col_count=(len(display_columns),'fixed'),
222 | wrap=True, max_rows=1000, show_label=False, interactive=True, elem_id="style_editor_grid")
223 |
224 | cls.search_and_replace_button.click(fn=cls.handle_search_and_replace_click, inputs=[cls.search_box, cls.replace_box, cls.dataeditor], outputs=cls.dataeditor)
225 |
226 | cls.filter_textbox.change(fn=None, inputs=[cls.filter_textbox, cls.filter_select], _js="filter_style_list")
227 | cls.filter_select.change(fn=None, inputs=[cls.filter_textbox, cls.filter_select], _js="filter_style_list")
228 |
229 | cls.use_encryption_checkbox.change(fn=cls.handle_use_encryption_checkbox_changed, inputs=[cls.use_encryption_checkbox], outputs=[dummy_component], _js="encryption_change")
230 | cls.encryption_key_textbox.change(fn=cls.handle_encryption_key_change, inputs=[cls.encryption_key_textbox], outputs=[])
231 | cls.restore_backup_file_upload.upload(fn=cls.handle_restore_backup_file_upload, inputs=[cls.restore_backup_file_upload], outputs=[cls.restore_result, cls.use_additional_styles_checkbox, cls.dataeditor])
232 | cls.restore_backup_file_upload.clear(fn=cls.handle_restore_backup_file_clear, inputs=[], outputs=[cls.restore_result])
233 | cls.backup_selection.change(fn=cls.handle_backup_selection_change, inputs=[cls.backup_selection], outputs=[cls.backup_selection, cls.restore_backup_file_upload])
234 | cls.backup_restore_button.click(fn=cls.handle_backup_restore_button_click, inputs=[cls.backup_selection], outputs=[cls.restore_result, cls.use_additional_styles_checkbox, cls.dataeditor])
235 | cls.dataeditor.change(fn=None, inputs=[cls.filter_textbox, cls.filter_select], _js="filter_style_list")
236 |
237 | cls.dataeditor.input(fn=cls.handle_dataeditor_input, inputs=[cls.dataeditor, cls.autosort_checkbox], outputs=cls.dataeditor)
238 | cls.autosort_checkbox.change(fn=cls.handle_autosort_checkbox_change, inputs=[cls.dataeditor, cls.autosort_checkbox], outputs=cls.dataeditor)
239 |
240 | style_editor.load(fn=None, _js="when_loaded")
241 | style_editor.load(fn=cls.backup.start, inputs=[], outputs=[])
242 |
243 | cls.use_additional_styles_checkbox.change(fn=cls.handle_use_additional_styles_box_change, inputs=[cls.use_additional_styles_checkbox, cls.style_file_selection],
244 | outputs=[cls.additional_file_display, cls.dataeditor, cls.style_file_selection])
245 | cls.style_file_selection.change(fn=cls.handle_style_file_selection_change, inputs=[cls.style_file_selection, dummy_component],
246 | outputs=[cls.dataeditor,cls.style_file_selection], _js="style_file_selection_change")
247 |
248 | cls.do_api.click(fn=cls.handle_outstanding_api_calls,outputs=cls.dataeditor)
249 |
250 | return [(style_editor, "Style Editor", "style_editor")]
251 |
252 | @classmethod
253 | def on_app_started(cls, block:gr.Blocks, api:FastAPI):
254 |
255 | @api.post("/style-editor/delete-style/")
256 | def delete_style(stylename:ParameterString):
257 | with cls.api_lock:
258 | cls.api_calls_outstanding.append(("delete",stylename.value))
259 |
260 | @api.post("/style-editor/duplicate-style/")
261 | def duplicate_style(stylename:ParameterString):
262 | with cls.api_lock:
263 | cls.api_calls_outstanding.append(("duplicate",stylename.value))
264 |
265 | @api.post("/style-editor/move-style/")
266 | def move_style(style:ParameterString, new_prefix:ParameterString):
267 | with cls.api_lock:
268 | cls.api_calls_outstanding.append(("move",(style.value, new_prefix.value)))
269 |
270 | @api.post("/style-editor/check-api/")
271 | def check() -> ParameterBool:
272 | return ParameterBool(value=True)
273 |
274 | @api.exception_handler(RequestValidationError)
275 | async def validation_exception_handler(request: Request, exc: RequestValidationError):
276 | exc_str = f'{exc}'.replace('\n', ' ').replace(' ', ' ')
277 | content = {'status_code': 422, 'message': exc_str, 'data': None}
278 | return JSONResponse(content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
279 |
280 | with block:
281 | for tabs in block.children:
282 | if isinstance(tabs, gr.layouts.Tabs):
283 | for tab in tabs.children:
284 | if isinstance(tab, gr.layouts.Tab):
285 | if tab.id=="style_editor":
286 | tab.select(fn=cls.handle_this_tab_selected, outputs=cls.dataeditor)
287 | else:
288 | tab.select(fn=cls.handle_another_tab_selected)
289 | if tab.id=="txt2img" or tab.id=="img2img":
290 | tab.select(fn=None, inputs=tab, _js="press_refresh_button")
291 |
292 | script_callbacks.on_ui_tabs(StyleEditor.on_ui_tabs)
293 | script_callbacks.on_app_started(StyleEditor.on_app_started)
--------------------------------------------------------------------------------
/scripts/shared.py:
--------------------------------------------------------------------------------
1 | name_column = 'name'
2 | columns = [name_column,'prompt','negative_prompt']
3 | user_columns = ['prompt','negative_prompt','notes']
4 | display_columns = ['sort', name_column,'prompt','negative_prompt','notes']
5 | d_types = {name_column:str,'prompt':str,'negative_prompt':str}
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | # Things I hope to add
2 | Feel free to open an issue if there's something else you'd like!
3 |
4 | ## Crawl (it's broken if this isn't done)
5 |
6 | ## Walk (really ought to do these)
7 |
8 | ## Run (these might be nice)
9 | - add style from txt2img or img2img prompts [https://github.com/chrisgoringe/Styles-Editor/issues/77]
10 | - copy selected styles between tabs not just prompt (not really this extension, but a pain)
11 |
12 | ## Fly (probably never going to happen, but just imagine...)
13 | - remote sharing of styles in a public database
14 |
--------------------------------------------------------------------------------