diff --git a/javascript/state.app.js b/javascript/state.app.js new file mode 100644 index 0000000..2933141 --- /dev/null +++ b/javascript/state.app.js @@ -0,0 +1,4 @@ + +document.addEventListener('DOMContentLoaded', function() { + onUiLoaded(state.core.init); +}); diff --git a/javascript/state.constants.js b/javascript/state.constants.js new file mode 100644 index 0000000..bd737ce --- /dev/null +++ b/javascript/state.constants.js @@ -0,0 +1,5 @@ +window.state = window.state || {}; + +state.constants = { + LS_PREFIX: 'store-' +}; \ No newline at end of file diff --git a/javascript/state.core.js b/javascript/state.core.js new file mode 100644 index 0000000..74488b2 --- /dev/null +++ b/javascript/state.core.js @@ -0,0 +1,216 @@ +window.state = window.state || {}; + +state.core = (function () { + + 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', + }; + + let store = null; + + 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) { + + store = new state.Store(); + + loadUI(); + 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}`); + } + }); + } + + handleExtensions(config); + } + + function storeTab() { + store.set('tab', this.textContent); + bindTabEvents(); + } + + function bindTabEvents() { + const tabs = gradioApp().querySelectorAll('#tabs > div:first-child button'); + tabs.forEach(tab => { // dirty hack here + tab.removeEventListener('click', storeTab); + tab.addEventListener('click', storeTab); + }); + return tabs; + } + + function loadUI() { + + let toolbar = document.createElement("div"); + toolbar.style.minWidth = 0; + toolbar.className = "gr-box relative w-full border-solid border border-gray-200 gr-padded"; + + let resetBtn = document.createElement("button"); + resetBtn.innerHTML = "🔁"; + resetBtn.className = "gr-button gr-button-lg gr-button-tool"; + resetBtn.style.border = "none"; + resetBtn.title = "Reset State"; + resetBtn.addEventListener('click', function () { + let confirmed = confirm('Reset all state values?'); + if (confirmed) { + store.clearAll(); + alert('All state values deleted!'); + } + }); + + toolbar.appendChild(resetBtn); + + let quickSettings = gradioApp().getElementById("quicksettings"); + quickSettings.appendChild(toolbar); + } + + + function restoreTabs(config) { + + if (! config.hasSetting('tabs')) { + return; + } + + const tabs = bindTabEvents(); + const value = store.get('tab'); + + if (value) { + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].textContent === value) { + state.utils.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; + } + store.set(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(() => { + state.utils.triggerEvent(seedInput, 'change'); + }, 100); + }); + }); + }); + + let value = store.get(id); + + if (! value) { + return; + } + + forEach(function (event) { + switch (this.type) { + case 'checkbox': + this.checked = value === 'true'; + state.utils.triggerEvent(this, event); + break; + case 'radio': + if (this.value === value) { + this.checked = true; + state.utils.triggerEvent(this, event); + } else { + this.checked = false; + } + break; + default: + this.value = value; + state.utils.triggerEvent(this, event); + } + }); + } + + function handleExtensions(config) { + if (config['state_extensions']) { + config['state_extensions'].forEach(function (ext) { + if (ext in state.extensions) { + state.extensions[ext].init(); + } + }); + } + } + + return { init }; +}()); diff --git a/javascript/state.ext.control-net.js b/javascript/state.ext.control-net.js new file mode 100644 index 0000000..ddcf946 --- /dev/null +++ b/javascript/state.ext.control-net.js @@ -0,0 +1,43 @@ +window.state = window.state || {}; +window.state.extensions = window.state.extensions || {}; + +state.extensions['control-net'] = (function () { + + let container = null; + let store = null; + + function bindTabEvents() { + const tabs = container.querySelectorAll('.tabs > div > button'); + tabs.forEach(tab => { // dirty hack here + tab.removeEventListener('click', onTabClick); + tab.addEventListener('click', onTabClick); + }); + return tabs; + } + + function handleTabs() { + let tabs = bindTabEvents(); + let value = store.get('tab'); + if (value) { + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].textContent === value) { + state.utils.triggerEvent(tabs[i], 'click'); + break; + } + } + } + } + + function onTabClick() { + store.set('tab', this.textContent); + bindTabEvents(); + } + + function init() { + container = gradioApp().getElementById('controlnet'); + store = new state.Store('ext-control-net'); + handleTabs(); + } + + return { init } +}()); diff --git a/javascript/state.store.js b/javascript/state.store.js new file mode 100644 index 0000000..6d315e6 --- /dev/null +++ b/javascript/state.store.js @@ -0,0 +1,30 @@ +window.state = window.state || {}; + +state.Store = function Store (prefix) { + this.prefix = state.constants.LS_PREFIX + (prefix ? prefix + '-' : ''); +} + +state.Store.prototype.set = function(key, value) { + localStorage.setItem(this.prefix + key, value); +}; + +state.Store.prototype.get = function(key) { + return localStorage.getItem(this.prefix + key); +}; + +state.Store.prototype.remove = function(key) { + localStorage.removeItem(this.prefix + key); +}; + +state.Store.prototype.clear = function() { + localStorage.clear(); +}; + +state.Store.prototype.clearAll = function () { + let keys = Object.keys(localStorage); + for (let i = 0; i < keys.length; i++) { + if (keys[i].startsWith(state.constants.LS_PREFIX)) { + localStorage.removeItem(keys[i]); + } + } +}; diff --git a/javascript/state.utils.js b/javascript/state.utils.js new file mode 100644 index 0000000..329e5b7 --- /dev/null +++ b/javascript/state.utils.js @@ -0,0 +1,11 @@ +window.state = window.state || {}; + +state.utils = { + triggerEvent: function triggerEvent(element, event) { + if (! element) { + return; + } + element.dispatchEvent(new Event(event.trim())); + return element; + } +};