diff --git a/ui/index.html b/ui/index.html index 8d72d0e4..bc176249 100644 --- a/ui/index.html +++ b/ui/index.html @@ -13,6 +13,7 @@ + @@ -125,10 +126,9 @@ Image Settings - - + + + Click to learn more about custom models - + Click to learn more about VAEs @@ -210,9 +208,7 @@
- + @@ -425,6 +421,7 @@ + diff --git a/ui/media/css/searchable-models.css b/ui/media/css/searchable-models.css new file mode 100644 index 00000000..c79919ad --- /dev/null +++ b/ui/media/css/searchable-models.css @@ -0,0 +1,91 @@ +.model-list { + position: absolute; + margin-block-start: 2px; + display: none; + padding-inline-start: 0; + max-height: 200px; + overflow: auto; + background: var(--input-background-color); + border: var(--input-border-size) solid var(--input-border-color); + border-radius: var(--input-border-radius); + color: var(--input-text-color); + z-index: 1; + line-height: normal; +} + +.model-list ul { + padding-right: 20px; + padding-inline-start: 0; + padding-bottom: 0px; +} + +.model-list li { + padding-bottom: 0px; +} + +.model-result { + list-style: none; +} + +.model-no-result { + color: var(--text-color); + list-style: none; + padding: 3px 6px 3px 6px; + font-size: 10pt; + font-style: italic; + display: none; +} + +.model-list li.model-folder { + color: var(--text-color); + list-style: none; + padding: 6px 6px 0px 6px; + font-size: 9pt; + font-weight: bold; +} + +.model-list li.model-file { + color: var(--input-text-color); + list-style: none; + padding-left: 12px; + font-size: 9pt; + font-weight: normal; + transition: none; + transition:property: none; + cursor: default; +} + +.model-list li.model-file.in-root-folder { + padding-left: 6px; +} + +.model-list li.model-file.selected { + background: grey; +} + +.model-selector { + cursor: pointer; +} + +.model-selector-arrow { + position: absolute; + width: 17px; + margin: 5px -17px; + padding-top: 3px; + cursor: pointer; + font-size: 8pt; +} + +.model-input { + white-space: nowrap; +} + +.reloadModels { + background: var(--background-color2); + border: none; + padding: 0px 0px; +} + +#reload-models.secondaryButton:hover { + background: var(--background-color2); +} diff --git a/ui/media/js/auto-save.js b/ui/media/js/auto-save.js index 18379eed..2ac853de 100644 --- a/ui/media/js/auto-save.js +++ b/ui/media/js/auto-save.js @@ -92,6 +92,9 @@ async function initSettings() { } function getSetting(element) { + if (element.dataset && 'path' in element.dataset) { + return element.dataset.path + } if (typeof element === "string" || element instanceof String) { element = SETTINGS[element].element } @@ -101,6 +104,10 @@ function getSetting(element) { return element.value } function setSetting(element, value) { + if (element.dataset && 'path' in element.dataset) { + element.dataset.path = value + return // no need to dispatch any event here because the models are not loaded yet + } if (typeof element === "string" || element instanceof String) { element = SETTINGS[element].element } diff --git a/ui/media/js/main.js b/ui/media/js/main.js index e515787b..7e31a97b 100644 --- a/ui/media/js/main.js +++ b/ui/media/js/main.js @@ -36,9 +36,9 @@ let useFaceCorrectionField = document.querySelector("#use_face_correction") let useUpscalingField = document.querySelector("#use_upscale") let upscaleModelField = document.querySelector("#upscale_model") let upscaleAmountField = document.querySelector("#upscale_amount") -let stableDiffusionModelField = document.querySelector('#stable_diffusion_model') -let vaeModelField = document.querySelector('#vae_model') -let hypernetworkModelField = document.querySelector('#hypernetwork_model') +let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion') +let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None') +let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None') let hypernetworkStrengthSlider = document.querySelector('#hypernetwork_strength_slider') let hypernetworkStrengthField = document.querySelector('#hypernetwork_strength') let outputFormatField = document.querySelector('#output_format') @@ -1279,71 +1279,6 @@ outputFormatField.addEventListener('change', e => { } }) -async function getModels() { - try { - const sd_model_setting_key = "stable_diffusion_model" - const vae_model_setting_key = "vae_model" - const hypernetwork_model_key = "hypernetwork_model" - const selectedSDModel = SETTINGS[sd_model_setting_key].value - const selectedVaeModel = SETTINGS[vae_model_setting_key].value - const selectedHypernetworkModel = SETTINGS[hypernetwork_model_key].value - - const models = await SD.getModels() - const modelsOptions = models['options'] - if ("scan-error" in models) { - // let previewPane = document.getElementById('tab-content-wrapper') - let previewPane = document.getElementById('preview') - previewPane.style.background="red" - previewPane.style.textAlign="center" - previewPane.innerHTML = '

🔥Malware alert!🔥

The file ' + models['scan-error'] + ' in your models/stable-diffusion folder is probably malware infected.

Please delete this file from the folder before proceeding!

After deleting the file, reload this page.

' - makeImageBtn.disabled = true - } - - const stableDiffusionOptions = modelsOptions['stable-diffusion'] - const vaeOptions = modelsOptions['vae'] - const hypernetworkOptions = modelsOptions['hypernetwork'] - - vaeOptions.unshift('') // add a None option - hypernetworkOptions.unshift('') // add a None option - - function createModelOptions(modelField, selectedModel, path="") { - return function fn(modelName) { - if (typeof(modelName) == 'string') { - const modelOption = document.createElement('option') - modelOption.value = path + modelName - modelOption.innerHTML = modelName !== '' ? (path != "" ? "  "+modelName : modelName) : 'None' - - if (path + modelName === selectedModel) { - modelOption.selected = true - } - modelField.appendChild(modelOption) - } else { - const modelGroup = document.createElement('optgroup') - modelGroup.label = path + modelName[0] - modelField.appendChild(modelGroup) - modelName[1].forEach( createModelOptions(modelField, selectedModel, path + modelName[0] + "/" ) ) - } - } - } - - stableDiffusionOptions.forEach(createModelOptions(stableDiffusionModelField, selectedSDModel)) - vaeOptions.forEach(createModelOptions(vaeModelField, selectedVaeModel)) - hypernetworkOptions.forEach(createModelOptions(hypernetworkModelField, selectedHypernetworkModel)) - - stableDiffusionModelField.dispatchEvent(new Event('change')) - vaeModelField.dispatchEvent(new Event('change')) - hypernetworkModelField.dispatchEvent(new Event('change')) - - // TODO: set default for model here too - SETTINGS[sd_model_setting_key].default = stableDiffusionOptions[0] - if (getSetting(sd_model_setting_key) == '' || SETTINGS[sd_model_setting_key].value == '') { - setSetting(sd_model_setting_key, stableDiffusionOptions[0]) - } - } catch (e) { - console.log('get models error', e) - } -} - function checkRandomSeed() { if (randomSeedField.checked) { seedField.disabled = true diff --git a/ui/media/js/searchable-models.js b/ui/media/js/searchable-models.js new file mode 100644 index 00000000..c26c004b --- /dev/null +++ b/ui/media/js/searchable-models.js @@ -0,0 +1,587 @@ +"use strict" + +let modelsCache +let modelsOptions + +/* +*** SEARCHABLE MODELS *** +Creates searchable dropdowns for SD, VAE, or HN models. +Also adds a reload models button (placed next to SD models, reloads everything including VAE and HN models). +More reload buttons may be added at strategic UI locations as needed. +Merely calling getModels() makes all the magic happen behind the scene to refresh the dropdowns. + +HOW TO CREATE A MODEL DROPDOWN: +1) Create an input element. Make sure to add a data-path property, as this is how model dropdowns are identified in auto-save.js. + + +2) Just declare one of these for your own dropdown (remember to change the element id, e.g. #stable_diffusion_models to your own input's id). +let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion') +let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None') +let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None') + +3) Model dropdowns will be refreshed automatically when the reload models button is invoked. +*/ +class ModelDropdown +{ + modelFilter //= document.querySelector("#model-filter") + modelFilterArrow //= document.querySelector("#model-filter-arrow") + modelList //= document.querySelector("#model-list") + modelResult //= document.querySelector("#model-result") + modelNoResult //= document.querySelector("#model-no-result") + + currentSelection //= { elem: undefined, value: '', path: ''} + highlightedModelEntry //= undefined + activeModel //= undefined + + inputModels //= undefined + modelKey //= undefined + flatModelList //= [] + noneEntry //= '' + + /* MIMIC A REGULAR INPUT FIELD */ + get parentElement() { + return this.modelFilter.parentElement + } + get parentNode() { + return this.modelFilter.parentNode + } + get value() { + return this.modelFilter.dataset.path + } + set value(path) { + this.modelFilter.dataset.path = path + this.selectEntry(path) + } + addEventListener(type, listener, options) { + return this.modelFilter.addEventListener(type, listener, options) + } + dispatchEvent(event) { + return this.modelFilter.dispatchEvent(event) + } + appendChild(option) { + // do nothing + } + + // remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html + bind(f, obj) { + return function() { + return f.apply(obj, arguments) + } + } + + /* SEARCHABLE INPUT */ + constructor (input, modelKey, noneEntry = '') { + this.modelFilter = input + this.noneEntry = noneEntry + this.modelKey = modelKey + + if (modelsOptions !== undefined) { // reuse models from cache (only useful for plugins, which are loaded after models) + this.inputModels = modelsOptions[this.modelKey] + this.populateModels() + } + document.addEventListener("refreshModels", this.bind(function(e) { + // reload the models + this.inputModels = modelsOptions[this.modelKey] + this.populateModels() + }, this)) + } + + saveCurrentSelection(elem, value, path) { + this.currentSelection.elem = elem + this.currentSelection.value = value + this.currentSelection.path = path + this.modelFilter.dataset.path = path + this.modelFilter.value = value + this.modelFilter.dispatchEvent(new Event('change')) + } + + processClick(e) { + e.preventDefault() + if (e.srcElement.classList.contains('model-file')) { + this.saveCurrentSelection(e.srcElement, e.srcElement.innerText, e.srcElement.dataset.path) + this.hideModelList() + this.modelFilter.focus() + this.modelFilter.select() + } + } + + findPreviousSibling(elem, previous = true) { + let sibling = previous ? elem.previousElementSibling : elem + let lastSibling = elem + + while (sibling && sibling.classList.contains('model-file')) { + if (sibling.style.display == 'list-item') return sibling + lastSibling = sibling + sibling = sibling.previousElementSibling + } + + // no more siblings, look for previous parent if any + if (sibling && sibling.classList.contains('model-folder')) { + return this.findPreviousSibling(sibling.firstElementChild.lastElementChild, false) + } + else if (lastSibling.parentElement.parentElement && lastSibling.parentElement.parentElement.previousElementSibling && lastSibling.parentElement.parentElement.previousElementSibling.firstElementChild && lastSibling.parentElement.parentElement.previousElementSibling.firstElementChild.lastElementChild) { + return this.findPreviousSibling(lastSibling.parentElement.parentElement.previousElementSibling.firstElementChild.lastElementChild, false) + } + else if (lastSibling.parentElement.parentElement.previousElementSibling) { + return this.findPreviousSibling(lastSibling.parentElement.parentElement.previousElementSibling, false) + } + } + + findNextSibling(elem, next = true) { + let sibling = next ? elem.nextElementSibling : elem + let lastSibling = elem + + while (sibling && sibling.classList.contains('model-file')) { + if (sibling.style.display == 'list-item') return sibling + lastSibling = sibling + sibling = sibling.nextElementSibling + } + + // no more siblings, look for next parent if any + if (lastSibling.nextElementSibling) { + return this.findNextSibling(lastSibling.nextElementSibling.firstElementChild.firstElementChild, false) + } + else if (lastSibling.parentElement.parentElement.nextElementSibling && lastSibling.parentElement.parentElement.nextElementSibling.firstElementChild && lastSibling.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild) { + return this.findNextSibling(lastSibling.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild, false) + } + else if (lastSibling.parentElement.parentElement.nextElementSibling && lastSibling.parentElement.parentElement.nextElementSibling.firstElementChild && lastSibling.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild) { + return this.findNextSibling(lastSibling.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild, false) + } + else if (lastSibling.parentElement.parentElement.nextElementSibling && lastSibling.parentElement.parentElement.nextElementSibling) { + return this.findNextSibling(lastSibling.parentElement.parentElement.nextElementSibling, false) + } + } + + selectModelEntry(elem) { + if (elem) { + if (this.highlightedModelEntry !== undefined) { + this.highlightedModelEntry.classList.remove('selected') + } + this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path) + elem.classList.add('selected') + elem.scrollIntoView({block: 'nearest'}) + this.highlightedModelEntry = elem + } + } + + selectPreviousFile() { + const elem = this.findPreviousSibling(this.highlightedModelEntry) + if (elem) { + this.selectModelEntry(elem) + } + else + { + this.highlightedModelEntry.parentElement.parentElement.scrollIntoView({block: 'nearest'}) + } + this.modelFilter.select() + } + + selectNextFile() { + this.selectModelEntry(this.findNextSibling(this.highlightedModelEntry)) + this.modelFilter.select() + } + + selectFirstFile() { + this.selectModelEntry(this.modelList.querySelector('.model-file')) + this.highlightedModelEntry.scrollIntoView({block: 'nearest'}) + this.modelFilter.select() + } + + selectLastFile() { + const elems = this.modelList.querySelectorAll('.model-file:last-child') + this.selectModelEntry(elems[elems.length -1]) + this.modelFilter.select() + } + + resetSelection() { + this.hideModelList() + this.showAllEntries() + this.modelFilter.value = this.currentSelection.value + this.modelFilter.focus() + this.modelFilter.select() + } + + validEntrySelected() { + return (this.modelNoResult.style.display === 'none') + } + + processKey(e) { + switch (e.key) { + case 'Escape': + e.preventDefault() + this.resetSelection() + break + case 'Enter': + e.preventDefault() + if (this.validEntrySelected()) { + if (this.modelList.style.display != 'block') { + this.showModelList() + } + else + { + this.saveCurrentSelection(this.highlightedModelEntry, this.highlightedModelEntry.innerText, this.highlightedModelEntry.dataset.path) + this.hideModelList() + } + this.modelFilter.focus() + } + else + { + this.resetSelection() + } + break + case 'ArrowUp': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectPreviousFile() + } + break + case 'ArrowDown': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectNextFile() + } + break + case 'ArrowLeft': + if (this.modelList.style.display != 'block') { + e.preventDefault() + } + break + case 'ArrowRight': + if (this.modelList.style.display != 'block') { + e.preventDefault() + } + break + case 'PageUp': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + } + break + case 'PageDown': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + } + break + case 'Home': + //if (this.modelList.style.display != 'block') { + e.preventDefault() + if (this.validEntrySelected()) { + this.selectFirstFile() + } + //} + break + case 'End': + //if (this.modelList.style.display != 'block') { + e.preventDefault() + if (this.validEntrySelected()) { + this.selectLastFile() + } + //} + break + default: + //console.log(e.key) + } + } + + modelListFocus() { + this.selectEntry() + this.showAllEntries() + } + + showModelList() { + this.modelList.style.display = 'block' + this.selectEntry() + this.showAllEntries() + this.modelFilter.value = '' + this.modelFilter.focus() + this.modelFilter.style.cursor = 'auto' + } + + hideModelList() { + this.modelList.style.display = 'none' + this.modelFilter.value = this.currentSelection.value + this.modelFilter.style.cursor = '' + } + + toggleModelList(e) { + e.preventDefault() + if (this.modelList.style.display != 'block') { + this.showModelList() + } + else + { + this.hideModelList() + this.modelFilter.select() + } + } + + selectEntry(path) { + if (path !== undefined) { + const entries = this.modelList.querySelectorAll('.model-file'); + + for (let i = 0; i < entries.length; i++) { + if (entries[i].dataset.path == path) { + this.saveCurrentSelection(entries[i], entries[i].innerText, entries[i].dataset.path) + this.highlightedModelEntry = entries[i] + entries[i].scrollIntoView({block: 'nearest'}) + break + } + } + } + + if (this.currentSelection.elem !== undefined) { + // select the previous element + if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != this.currentSelection.elem) { + this.highlightedModelEntry.classList.remove('selected') + } + this.currentSelection.elem.classList.add('selected') + this.highlightedModelEntry = this.currentSelection.elem + this.currentSelection.elem.scrollIntoView({block: 'nearest'}) + } + else + { + this.selectFirstFile() + } + } + + highlightModelAtPosition(e) { + let elem = document.elementFromPoint(e.clientX, e.clientY) + + if (elem.classList.contains('model-file')) { + this.highlightModel(elem) + } + } + + highlightModel(elem) { + if (elem.classList.contains('model-file')) { + if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != elem) { + this.highlightedModelEntry.classList.remove('selected') + } + elem.classList.add('selected') + this.highlightedModelEntry = elem + } + } + + showAllEntries() { + this.modelList.querySelectorAll('li').forEach(function(li) { + if (li.id !== 'model-no-result') { + li.style.display = 'list-item' + } + }) + this.modelNoResult.style.display = 'none' + } + + filterList(e) { + const filter = this.modelFilter.value.toLowerCase() + let found = false + let showAllChildren = false + + this.modelList.querySelectorAll('li').forEach(function(li) { + if (li.classList.contains('model-folder')) { + showAllChildren = false + } + if (filter == '') { + li.style.display = 'list-item' + found = true + } else if (showAllChildren || li.textContent.toLowerCase().match(filter)) { + li.style.display = 'list-item' + if (li.classList.contains('model-folder') && li.firstChild.textContent.toLowerCase().match(filter)) { + showAllChildren = true + } + found = true + } else { + li.style.display = 'none' + } + }) + + if (found) { + this.modelResult.style.display = 'list-item' + this.modelNoResult.style.display = 'none' + const elem = this.findNextSibling(this.modelList.querySelector('.model-file'), false) + this.highlightModel(elem) + elem.scrollIntoView({block: 'nearest'}) + } + else + { + this.modelResult.style.display = 'none' + this.modelNoResult.style.display = 'list-item' + } + this.modelList.style.display = 'block' + } + + /* MODEL LOADER */ + flattenModelList(models, path) { + models.forEach(entry => { + if (Array.isArray(entry)) { + this.flattenModelList(entry[1], path + '/' + entry[0]) + } + else + { + this.flatModelList.push(path == '' ? entry : path + '/' + entry) + } + }) + } + + // sort models + getFolder(model) { + return model.substring(0, model.lastIndexOf('/') + 1) + } + + sortModels(models) { + let found + do { + found = false + for (let i = 0; i < models.length - 1; i++) { + if ( + (this.getFolder(models[i]) == this.getFolder(models[i+1]) && models[i].toLowerCase() > models[i+1].toLowerCase()) // same folder, sort by alphabetical order + || (this.getFolder(models[i]).toLowerCase() > this.getFolder(models[i+1]).toLowerCase()) // L1 folder > L2 folder + ) { + [models[i], models[i+1]] = [models[i+1], models[i]]; + found = true + } + } + } while (found) + } + + populateModels() { + this.activeModel = this.modelFilter.dataset.path + + this.currentSelection = { elem: undefined, value: '', path: ''} + this.highlightedModelEntry = undefined + this.flatModelList = [] + + if(this.modelList !== undefined) { + this.modelList.remove() + this.modelFilterArrow.remove() + } + this.createDropdown() + } + + createDropdown() { + // prepare to sort the models + this.flattenModelList(this.inputModels, '') + this.sortModels(this.flatModelList) + + // create dropdown entries + this.modelFilter.insertAdjacentHTML('afterend', this.parseModels(this.flatModelList)) + this.modelFilter.classList.add('model-selector') + this.modelFilterArrow = document.querySelector(`#${this.modelFilter.id}-model-filter-arrow`) + this.modelList = document.querySelector(`#${this.modelFilter.id}-model-list`) + this.modelResult = document.querySelector(`#${this.modelFilter.id}-model-result`) + this.modelNoResult = document.querySelector(`#${this.modelFilter.id}-model-no-result`) + this.modelList.style.display = 'block' + this.modelList.style.paddingRight = '20px' + this.modelFilter.style.width = this.modelList.offsetWidth + 'px' + this.modelFilterArrow.style.height = this.modelFilter.offsetHeight + 'px' + this.modelList.style.display = 'none' + + this.modelFilter.addEventListener('input', this.bind(this.filterList, this)) + this.modelFilter.addEventListener('focus', this.bind(this.modelListFocus, this)) + this.modelFilter.addEventListener('blur', this.bind(this.hideModelList, this)) + this.modelFilter.addEventListener('click', this.bind(this.showModelList, this)) + this.modelFilter.addEventListener('keydown', this.bind(this.processKey, this)) + this.modelFilterArrow.addEventListener('mousedown', this.bind(this.toggleModelList, this)) + this.modelList.addEventListener('mousemove', this.bind(this.highlightModelAtPosition, this)) + this.modelList.addEventListener('mousedown', this.bind(this.processClick, this)) + + this.selectEntry(this.activeModel) + } + + parseModels(models) { + let html = ` + ' + } + + html += ` + + + + ` + return html + } +} + +/* (RE)LOAD THE MODELS */ +async function getModels() { + try { + modelsCache = await SD.getModels() + modelsOptions = modelsCache['options'] + if ("scan-error" in modelsCache) { + // let previewPane = document.getElementById('tab-content-wrapper') + let previewPane = document.getElementById('preview') + previewPane.style.background="red" + previewPane.style.textAlign="center" + previewPane.innerHTML = '

🔥Malware alert!🔥

The file ' + modelsCache['scan-error'] + ' in your models/stable-diffusion folder is probably malware infected.

Please delete this file from the folder before proceeding!

After deleting the file, reload this page.

' + makeImageBtn.disabled = true + } + + /* This code should no longer be needed. Commenting out for now, will cleanup later. + const sd_model_setting_key = "stable_diffusion_model" + const vae_model_setting_key = "vae_model" + const hypernetwork_model_key = "hypernetwork_model" + + const stableDiffusionOptions = modelsOptions['stable-diffusion'] + const vaeOptions = modelsOptions['vae'] + const hypernetworkOptions = modelsOptions['hypernetwork'] + + // TODO: set default for model here too + SETTINGS[sd_model_setting_key].default = stableDiffusionOptions[0] + if (getSetting(sd_model_setting_key) == '' || SETTINGS[sd_model_setting_key].value == '') { + setSetting(sd_model_setting_key, stableDiffusionOptions[0]) + } + */ + + // notify ModelDropdown objects to refresh + document.dispatchEvent(new Event('refreshModels')) + } catch (e) { + console.log('get models error', e) + } +} + +// reload models button +document.querySelector('#reload-models').addEventListener('click', getModels)