From 2cda410b41f13a9157c46a96e5564f709d051cce Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 8 Dec 2022 18:23:17 -0300 Subject: [PATCH] add custom select element with autocomplete Same as a select, but you can kind of search for the item you want. Good for when you have a lot of models. Signed-off-by: Victor Seiji Hariki --- css/index.css | 4 + css/ui/generic.css | 47 ++++++++++ index.html | 17 ++-- js/index.js | 207 ++++++++++++++++++++++----------------------- js/lib/ui.js | 173 +++++++++++++++++++++++++++++++++++++ 5 files changed, 329 insertions(+), 119 deletions(-) diff --git a/css/index.css b/css/index.css index 64b7cc8..9a4148a 100644 --- a/css/index.css +++ b/css/index.css @@ -35,6 +35,10 @@ body { margin-bottom: 5px; } +.display-none { + display: none; +} + .collapsible:hover { background-color: #777; } diff --git a/css/ui/generic.css b/css/ui/generic.css index a7c2271..ee709c4 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -40,6 +40,8 @@ div.slider-wrapper { position: relative; height: 20px; border-radius: 5px; + + overflow-y: visible; } div.slider-wrapper * { @@ -95,6 +97,51 @@ div.slider-wrapper > input.text { background-color: transparent; } +/* Autocomplete Select */ +div.autocomplete { + border-radius: 5px; +} + +div.autocomplete > .autocomplete-text { + box-sizing: border-box; + + border-radius: 5px; + + width: 100%; +} + +div.autocomplete > .autocomplete-list { + position: absolute; + + background-color: white; + + overflow-y: auto; + + margin-top: 0; + margin-left: 0; + + max-height: 200px; + min-width: 100%; + max-width: 800px; + + width: fit-content; + z-index: 200; +} + +div.autocomplete > .autocomplete-list > .autocomplete-option { + cursor: pointer; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + padding: 3px; +} + +div.autocomplete > .autocomplete-list > .autocomplete-option:hover { + background-color: #dddf; +} + /* Select Input */ select > option:checked::after { content: ""; diff --git a/index.html b/index.html index ab6c932..d1cd6d5 100644 --- a/index.html +++ b/index.html @@ -70,15 +70,10 @@ Stable Diffusion settings
- - -
- - -
+ +
+ +


- - + +
diff --git a/js/index.js b/js/index.js index 480caa8..9ca7c2d 100644 --- a/js/index.js +++ b/js/index.js @@ -111,7 +111,6 @@ function startup() { }; drawBackground(); - changeSampler(); changeMaskBlur(); changeSmoothRendering(); changeSeed(); @@ -392,16 +391,6 @@ function drawMarchingAnts(ctx, bb, offset, options) { ctx.restore(); } -function changeSampler() { - if (!document.getElementById("samplerSelect").value == "") { - // must be done, since before getSamplers is done, the options are empty - console.log(document.getElementById("samplerSelect").value == ""); - stableDiffusionData.sampler_index = - document.getElementById("samplerSelect").value; - localStorage.setItem("sampler", stableDiffusionData.sampler_index); - } -} - const makeSlider = ( label, el, @@ -435,6 +424,21 @@ const makeSlider = ( }); }; +const modelAutoComplete = createAutoComplete( + "Model", + document.getElementById("models-ac-select") +); + +const samplerAutoComplete = createAutoComplete( + "Sampler", + document.getElementById("sampler-ac-select") +); + +const upscalerAutoComplete = createAutoComplete( + "Upscaler", + document.getElementById("upscaler-ac-select") +); + makeSlider( "Resolution", document.getElementById("resolution"), @@ -528,7 +532,7 @@ function drawBackground() { } } -function getUpscalers() { +async function getUpscalers() { /* so for some reason when upscalers request returns upscalers, the real-esrgan model names are incorrect, and need to be fetched from /sdapi/v1/realesrgan-models also the realesrgan models returned are not all correct, extra fun! @@ -541,12 +545,9 @@ function getUpscalers() { */ // hacky way to get the correct list of upscalers - var upscalerSelect = document.getElementById("upscalers"); var extras_url = document.getElementById("host").value + "/sdapi/v1/extra-single-image/"; // endpoint for upscaling, needed for the hacky way to get the correct list of upscalers - var empty_image = new Image(512, 512); - empty_image.src = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAFCAAAAABCAYAAAChpRsuAAAALklEQVR42u3BAQ0AAAgDoJvc6LeHAybtBgAAAAAAAAAAAAAAAAAAAAAAAAB47QD2wAJ/LnnqGgAAAABJRU5ErkJggg=="; //transparent pixel + var empty_image = new Image(1, 1); var purposefully_incorrect_data = { "resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options upscaling_resize: 2, @@ -554,27 +555,36 @@ function getUpscalers() { image: empty_image.src, }; - fetch(extras_url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(purposefully_incorrect_data), - }) - .then((response) => response.json()) - .then((data) => { - console.log("purposefully_incorrect_data response, ignore above error"); - // result = purposefully_incorrect_data response: Invalid upscaler, needs to be on of these: None , Lanczos , Nearest , LDSR , BSRGAN , R-ESRGAN General 4xV3 , R-ESRGAN 4x+ Anime6B , ScuNET , ScuNET PSNR , SwinIR_4x - let upscalers = data.detail.split(": ")[1].trim().split(" , "); // converting the result to a list of upscalers - for (var i = 0; i < upscalers.length; i++) { - // if(upscalers[i] == "LDSR") continue; // Skip LDSR, see reason in the first comment // readded because worksonmymachine.jpg but leaving it here in case of, uh, future disaster? - var option = document.createElement("option"); - option.text = upscalers[i]; - option.value = upscalers[i]; - upscalerSelect.add(option); - } + try { + const response = await fetch(extras_url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(purposefully_incorrect_data), }); + const data = await response.json(); + + console.log( + "[index] purposefully_incorrect_data response, ignore above error" + ); + // result = purposefully_incorrect_data response: Invalid upscaler, needs to be on of these: None , Lanczos , Nearest , LDSR , BSRGAN , R-ESRGAN General 4xV3 , R-ESRGAN 4x+ Anime6B , ScuNET , ScuNET PSNR , SwinIR_4x + const upscalers = data.detail + .split(": ")[1] + .split(",") + .map((v) => v.trim()) + .filter((v) => v !== "None"); // converting the result to a list of upscalers + + upscalerAutoComplete.options = upscalers.map((u) => { + return {name: u, value: u}; + }); + + upscalerAutoComplete.value = upscalers[0]; + } catch (e) { + console.warn("[index] Failed to fetch upscalers:"); + console.warn(e); + } /* THE NON HACKY WAY THAT I SIMPLY COULD NOT GET TO PRODUCE A LIST WITHOUT NON WORKING UPSCALERS, FEEL FREE TO TRY AND FIGURE IT OUT @@ -621,18 +631,14 @@ function getUpscalers() { } async function getModels() { - var modelSelect = document.getElementById("models"); var url = document.getElementById("host").value + "/sdapi/v1/sd-models"; await fetch(url) .then((response) => response.json()) .then((data) => { - //console.log(data); All models - for (var i = 0; i < data.length; i++) { - var option = document.createElement("option"); - option.text = data[i].model_name; - option.value = data[i].title; - modelSelect.add(option); - } + modelAutoComplete.options = data.map((option) => ({ + name: option.title, + value: option.title, + })); }); // get currently loaded model @@ -642,41 +648,34 @@ async function getModels() { .then((data) => { var model = data.sd_model_checkpoint; console.log("Current model: " + model); - modelSelect.value = model; + console.debug((modelAutoComplete.value = model)); }); -} -function changeModel() { - // change the model - console.log("changing model to " + document.getElementById("models").value); - var model_title = document.getElementById("models").value; - var payload = { - sd_model_checkpoint: model_title, - }; - var url = document.getElementById("host").value + "/sdapi/v1/options/"; - fetch(url, { - method: "POST", - mode: "cors", // no-cors, *cors, same-origin - cache: "default", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - redirect: "follow", // manual, *follow, error - referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }) - .then((response) => response.json()) - .then(() => { - alert("Model changed to " + model_title); - }) - .catch((error) => { + modelAutoComplete.onchange.on(async ({value}) => { + console.log(`[index] Changing model to [${value}]`); + var payload = { + sd_model_checkpoint: value, + }; + var url = document.getElementById("host").value + "/sdapi/v1/options/"; + try { + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + alert(`Model changed to [${value}]`); + } catch (e) { + console.warn("[index] Error changing model"); + console.warn(e); + alert( - "Error changing model, please check console for additional info\n" + - error + "Error changing model, please check console for additional information" ); - }); + } + }); } async function getConfig() { @@ -810,42 +809,40 @@ function changeStyles() { stableDiffusionData.styles = selectedString; } -function getSamplers() { - var samplerSelect = document.getElementById("samplerSelect"); +async function getSamplers() { var url = document.getElementById("host").value + "/sdapi/v1/samplers"; - fetch(url) - .then((response) => response.json()) - .then((data) => { - //console.log(data); All samplers - for (var i = 0; i < data.length; i++) { - // PLMS SAMPLER DOES NOT WORK FOR ANY IMAGES BEYOND FOR THE INITIAL IMAGE (for me at least), GIVES ASGI Exception; AttributeError: 'PLMSSampler' object has no attribute 'stochastic_encode' - var option = document.createElement("option"); - option.text = data[i].name; - option.value = data[i].name; - samplerSelect.add(option); - } - if (localStorage.getItem("sampler") != null) { - samplerSelect.value = localStorage.getItem("sampler"); - } else { - // needed now, as hardcoded sampler cant be guaranteed to be in the list - samplerSelect.value = data[0].name; - localStorage.setItem("sampler", samplerSelect.value); - } - }) - .catch((error) => { - alert( - "Error getting samplers, please check console for additional info\n" + - error - ); + try { + const response = await fetch(url); + const data = await response.json(); + samplerAutoComplete.options = data.map((sampler) => ({ + name: sampler.name, + value: sampler.name, + })); + + // Initial sampler + if (localStorage.getItem("sampler") != null) { + samplerAutoComplete.value = localStorage.getItem("sampler"); + } else { + samplerAutoComplete.value = data[0].name; + localStorage.setItem("sampler", samplerAutoComplete.value); + } + + samplerAutoComplete.onchange.on(({value}) => { + stableDiffusionData.sampler_index = value; + localStorage.setItem("sampler", value); }); + } catch (e) { + console.warn("[index] Failed to fetch samplers"); + console.warn(e); + } } async function upscaleAndDownload() { // Future improvements: some upscalers take a while to upscale, so we should show a loading bar or something, also a slider for the upscale amount // get cropped canvas, send it to upscaler, download result var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something - var upscaler = document.getElementById("upscalers").value; + var upscaler = upscalerAutoComplete.value; var croppedCanvas = cropCanvas( uil.getVisible({ x: 0, @@ -855,7 +852,6 @@ async function upscaleAndDownload() { }) ); if (croppedCanvas != null) { - var upscaler = document.getElementById("upscalers").value; var url = document.getElementById("host").value + "/sdapi/v1/extra-single-image/"; var imgdata = croppedCanvas.canvas.toDataURL("image/png"); @@ -903,10 +899,6 @@ function loadSettings() { localStorage.getItem("neg_prompt") == null ? "people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry" : localStorage.getItem("neg_prompt"); - var _sampler = - localStorage.getItem("sampler") == null - ? "DDIM" - : localStorage.getItem("sampler"); var _mask_blur = localStorage.getItem("mask_blur") == null ? 0 @@ -924,7 +916,6 @@ function loadSettings() { document.getElementById("prompt").title = String(_prompt); document.getElementById("negPrompt").value = String(_negprompt); document.getElementById("negPrompt").title = String(_negprompt); - document.getElementById("samplerSelect").value = String(_sampler); document.getElementById("maskBlur").value = Number(_mask_blur); document.getElementById("seed").value = Number(_seed); document.getElementById("cbxHRFix").checked = Boolean(_enable_hr); diff --git a/js/lib/ui.js b/js/lib/ui.js index 0fb0373..a039067 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -192,3 +192,176 @@ function createSlider(name, wrapper, options = {}) { }, }; } + +/** + * A function to transform a div into a autocompletable select element + * + * @param {string} name Name of the AutoComplete Select Element + * @param {HTMLDivElement} wrapper The div element that will wrap the input elements + * @param {object} options Extra options + * @param {{name: string, value: string}} options.options Options to add to the selector + * @returns {AutoCompleteElement} + */ +function createAutoComplete(name, wrapper, options = {}) { + defaultOpt(options, { + options: [], + }); + + wrapper.classList.add("autocomplete"); + + const inputEl = document.createElement("input"); + inputEl.type = "text"; + inputEl.classList.add("autocomplete-text"); + + const autocompleteEl = document.createElement("div"); + autocompleteEl.classList.add("autocomplete-list", "display-none"); + + let timeout = null; + let ontext = false; + let onlist = false; + + wrapper.appendChild(inputEl); + wrapper.appendChild(autocompleteEl); + + const acobj = { + name, + wrapper, + _title: null, + _value: null, + _options: [], + + /** @type {Observer<{name:string, value: string}>} */ + onchange: new Observer(), + + get value() { + return this._value; + }, + set value(val) { + const opt = this.options.find((option) => option.value === val); + + if (!opt) return; + + this._title = opt.name; + this._value = opt.value; + inputEl.value = opt.name; + inputEl.title = opt.name; + + this.onchange.emit({name: opt.name, value: opt.value}); + }, + + get options() { + return this._options; + }, + set options(val) { + console.debug(val); + this._options = []; + + while (autocompleteEl.lastChild) { + autocompleteEl.removeChild(autocompleteEl.lastChild); + } + + // Add options + val.forEach((opt) => { + const {name, value} = opt; + const option = {name, value}; + + const optionEl = document.createElement("option"); + optionEl.classList.add("autocomplete-option"); + optionEl.title = option.name; + optionEl.addEventListener("click", () => select(option)); + + this._options.push({name, value, optionElement: optionEl}); + + autocompleteEl.appendChild(optionEl); + }); + + updateOptions(); + }, + }; + + function updateOptions() { + const text = inputEl.value.toLowerCase().trim(); + + acobj._options.forEach((opt) => { + const textLocation = opt.name.toLowerCase().indexOf(text); + + while (opt.optionElement.lastChild) { + opt.optionElement.removeChild(opt.optionElement.lastChild); + } + + opt.optionElement.append( + document.createTextNode(opt.name.substring(0, textLocation)) + ); + const span = document.createElement("span"); + span.style.fontWeight = "bold"; + span.textContent = opt.name.substring( + textLocation, + textLocation + text.length + ); + opt.optionElement.appendChild(span); + opt.optionElement.appendChild( + document.createTextNode( + opt.name.substring(textLocation + text.length, opt.name.length) + ) + ); + + if (textLocation !== -1) { + opt.optionElement.classList.remove("display-none"); + } else opt.optionElement.classList.add("display-none"); + }); + } + + function select(options) { + ontext = false; + onlist = false; + + acobj._title = options.name; + inputEl.value = options.name; + acobj.value = options.value; + + autocompleteEl.classList.add("display-none"); + } + + inputEl.addEventListener("focus", () => { + ontext = true; + + autocompleteEl.classList.remove("display-none"); + inputEl.select(); + }); + inputEl.addEventListener("blur", () => { + ontext = false; + + if (!onlist && !ontext) { + inputEl.value = ""; + updateOptions(); + inputEl.value = acobj._title; + + autocompleteEl.classList.add("display-none"); + } + }); + + autocompleteEl.addEventListener("mouseenter", () => { + onlist = true; + }); + + autocompleteEl.addEventListener("mouseleave", () => { + onlist = false; + + if (!onlist && !ontext) { + inputEl.value = ""; + updateOptions(); + inputEl.value = acobj._title; + + autocompleteEl.classList.add("display-none"); + } + }); + + // Filter + inputEl.addEventListener("input", () => { + updateOptions(); + }); + + acobj.options = options.options; + + return acobj; +}