commit 89a7ebc894b5fe6cfe2ca93e906429cde31baaba Author: ilian.iliev Date: Sun Mar 19 15:51:25 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f1d055 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +__pycache__/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5102d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ilian Iliev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0600139 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ + + + +# stable-diffusion-webui-state + +This extension is for AUTOMATIC1111's [Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) + +### Capabilities + +* Preserve web UI parameters (inputs, sliders, checkboxes etc.) after page reload. +* It can be extended to preserve basically everything in the UI. + +### Install + +Use **Install from URL** option with this repo url. + +### Requirements + +None at all. + +### Usage + +Go to **Settings->State** and check all parameters that you want to be preserved after page reload. + +### Contributing + +Feel free to submit PRs to develop! \ No newline at end of file diff --git a/javascript/state.js b/javascript/state.js new file mode 100644 index 0000000..6c89911 --- /dev/null +++ b/javascript/state.js @@ -0,0 +1,179 @@ + +document.addEventListener('DOMContentLoaded', function() { + onUiLoaded(StateController.init); +}); + + +const StateController = (function () { + + const LS_PREFIX = 'state-'; + const TABS = ['txt2img', 'img2img']; + const ELEMENTS = { + 'prompt': 'prompt', + 'negative_prompt': 'neg_prompt', + 'sampling': 'sampling', + 'sampling_steps': 'steps', + 'restore_faces': 'restore_faces', + 'tiling': 'tiling', + 'hires_fix': 'enable_hr', + 'hires_upscaler': 'hr_upscaler', + 'hires_steps': 'hires_steps', + 'hires_scale': 'hr_scale', + 'hires_resize_x': 'hr_resize_x', + 'hires_resize_y': 'hr_resize_y', + 'hires_denoising_strength': 'denoising_strength', + 'width': 'width', + 'height': 'height', + 'batch_count': 'batch_count', + 'batch_size': 'batch_size', + 'cfg_scale': 'cfg_scale', + 'denoising_strength': 'denoising_strength', + 'seed': 'seed' + }; + + const ELEMENTS_WITHOUT_PREFIX = { + 'resize_mode': 'resize_mode', + }; + + function triggerEvent(element, event) { + if (! element) { + return; + } + element.dispatchEvent(new Event(event.trim())); + return element; + } + + function hasSetting(id, tab) { + const suffix = tab ? `_${tab}` : ''; + return this[`state${suffix}`] && this[`state${suffix}`].indexOf(id) > -1; + } + + function init() { + fetch('/state/config.json?_=' + (+new Date())) + .then(response => response.json()) + .then(config => { + try { + config.hasSetting = hasSetting + load(config); + } catch (error) { + console.error('[state]: Error:', error); + } + }) + .catch(error => console.error('[state]: Error getting JSON file:', error)); + } + + function load(config) { + + restoreTabs(config); + + for (const [settingId, element] of Object.entries(ELEMENTS)) { + TABS.forEach(tab => { + if (config.hasSetting(settingId, tab)) { + handleSavedInput(`${tab}_${element}`); + } + }); + } + + for (const [settingId, element] of Object.entries(ELEMENTS_WITHOUT_PREFIX)) { + TABS.forEach(tab => { + if (config.hasSetting(settingId, tab)) { + handleSavedInput(`${element}`); + } + }); + } + } + + function restoreTabs(config) { + + if (! config.hasSetting('tabs')) { + return; + } + + const tabs = gradioApp().querySelectorAll('#tabs > div:first-child button'); + + tabs.forEach(tab => { + tab.addEventListener('click', function () { + localStorage.setItem(LS_PREFIX + 'tab', this.textContent); + }); + }); + + const value = localStorage.getItem(LS_PREFIX + 'tab'); + + if (value) { + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].textContent === value) { + triggerEvent(tabs[i], 'click'); + break; + } + } + } + } + + function handleSavedInput(id) { + + const elements = gradioApp().querySelectorAll(`#${id} textarea, #${id} select, #${id} input`); + const events = ['change', 'input']; + + if (! elements || ! elements.length) { + return; + } + + let forEach = function (action) { + events.forEach(function(event) { + elements.forEach(function (element) { + action.call(element, event); + }); + }); + }; + + forEach(function (event) { + this.addEventListener(event, function () { + let value = this.value; + if (this.type && this.type === 'checkbox') { + value = this.checked; + } + localStorage.setItem(LS_PREFIX + id, value); + }); + }); + + TABS.forEach(tab => { + const seedInput = gradioApp().querySelector(`#${tab}_seed input`); + ['random_seed', 'reuse_seed'].forEach(id => { + const btn = gradioApp().querySelector(`#${tab}_${id}`); + btn.addEventListener('click', () => { + setTimeout(() => { + triggerEvent(seedInput, 'change'); + }, 100); + }); + }); + }); + + let value = localStorage.getItem(LS_PREFIX + id); + + if (! value) { + return; + } + + forEach(function (event) { + switch (this.type) { + case 'checkbox': + this.checked = value === 'true'; + triggerEvent(this, event); + break; + case 'radio': + if (this.value === value) { + this.checked = true; + triggerEvent(this, event); + } else { + this.checked = false; + } + break; + default: + this.value = value; + triggerEvent(this, event); + } + }); + } + + return { init }; +}()); diff --git a/scripts/state_api.py b/scripts/state_api.py new file mode 100644 index 0000000..4efcb7a --- /dev/null +++ b/scripts/state_api.py @@ -0,0 +1,30 @@ +from fastapi import FastAPI, Body, HTTPException, Request, Response +from fastapi.responses import FileResponse + +import gradio as gr +import modules.script_callbacks as script_callbacks + + +class StateApi(): + + BASE_PATH = '/state' + + def get_path(self, path): + return f"{self.BASE_PATH}{path}" + + def add_api_route(self, path: str, endpoint, **kwargs): + return self.app.add_api_route(self.get_path(path), endpoint, **kwargs) + + def start(self, _: gr.Blocks, app: FastAPI): + self.app = app + self.add_api_route('/config.json', self.get_config, methods=['GET']) + + def get_config(self): + return FileResponse('config.json') + + +try: + api = StateApi() + script_callbacks.on_app_started(api.start) +except: + pass \ No newline at end of file diff --git a/scripts/state_settings.py b/scripts/state_settings.py new file mode 100644 index 0000000..67ab324 --- /dev/null +++ b/scripts/state_settings.py @@ -0,0 +1,60 @@ +import gradio as gr +import modules.shared as shared +from modules import scripts + + +def on_ui_settings(): + + section = ("state", "State") + + shared.opts.add_option("state", shared.OptionInfo([], "Saved main elements", gr.CheckboxGroup, lambda: { + "choices": [ + "tabs" + ] + }, section=section)) + + shared.opts.add_option("state_txt2img", shared.OptionInfo([], "Saved elements from txt2img", gr.CheckboxGroup, lambda: { + "choices": [ + "prompt", + "negative_prompt", + "sampling", + "sampling_steps", + "width", + "height", + "batch_count", + "batch_size", + "cfg_scale", + "seed", + "restore_faces", + "tiling", + "hires_fix", + "hires_upscaler", + "hires_steps", + "hires_scale", + "hires_resize_x", + "hires_resize_y", + "hires_denoising_strength", + ] + }, section=section)) + + shared.opts.add_option("state_img2img", shared.OptionInfo([], "Saved elements from img2img", gr.CheckboxGroup, lambda: { + "choices": [ + "prompt", + "negative_prompt", + "sampling", + "resize_mode", + "sampling_steps", + "restore_faces", + "tiling", + "width", + "height", + "batch_count", + "batch_size", + "cfg_scale", + "denoising_strength", + "seed" + ] + }, section=section)) + + +scripts.script_callbacks.on_ui_settings(on_ui_settings)