Searchable models (#786)

* 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).

* Fixing the editor pane display

* Revert "Fixing the editor pane display"

This reverts commit de902a6340.

* Move formatting to the CSS file

* Rewritten the siblings functions

I like these much better, and I imagine you will too. :)

* Code cleanup

* Minor tweak in list ordering

Minor tweak to move the root folder's content at the end of the list (similar to the current version).
This commit is contained in:
patriceac 2023-02-12 01:18:09 -08:00 committed by GitHub
parent 04e8458ce2
commit a801a5d8b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 701 additions and 87 deletions

View File

@ -14,6 +14,7 @@
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css">
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css">
<link rel="stylesheet" href="/media/css/image-editor.css">
<link rel="stylesheet" href="/media/css/searchable-models.css">
<link rel="manifest" href="/media/manifest.webmanifest">
<script src="/media/js/jquery-3.6.1.min.js"></script>
<script src="/media/js/jquery-confirm.min.js"></script>
@ -125,10 +126,9 @@
<tr><b class="settings-subheader">Image Settings</b></tr>
<tr class="pl-5"><td><label for="seed">Seed:</label></td><td><input id="seed" name="seed" size="10" value="0" onkeypress="preventNonNumericalInput(event)"> <input id="random_seed" name="random_seed" type="checkbox" checked><label for="random_seed">Random</label></td></tr>
<tr class="pl-5"><td><label for="num_outputs_total">Number of Images:</label></td><td><input id="num_outputs_total" name="num_outputs_total" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label><small>(total)</small></label> <input id="num_outputs_parallel" name="num_outputs_parallel" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label for="num_outputs_parallel"><small>(in parallel)</small></label></td></tr>
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td>
<select id="stable_diffusion_model" name="stable_diffusion_model">
<!-- <option value="sd-v1-4" selected>sd-v1-4</option> -->
</select>
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td class="model-input">
<input id="stable_diffusion_model" type="text" spellcheck="false" class="model-filter" data-path="" />
<button id="reload-models" class="secondaryButton reloadModels"><i class='fa-solid fa-rotate'></i></button>
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about custom models</span></i></a>
</td></tr>
<!-- <tr id="modelConfigSelection" class="pl-5"><td><label for="model_config">Model Config:</i></label></td><td>
@ -136,9 +136,7 @@
</select>
</td></tr> -->
<tr class="pl-5"><td><label for="vae_model">Custom VAE:</i></label></td><td>
<select id="vae_model" name="vae_model">
<!-- <option value="" selected>None</option> -->
</select>
<input id="vae_model" type="text" spellcheck="false" class="model-filter" data-path="" />
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about VAEs</span></i></a>
</td></tr>
<tr id="samplerSelection" class="pl-5"><td><label for="sampler_name">Sampler:</label></td><td>
@ -210,9 +208,7 @@
<tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="11" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
<tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr>
<tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td>
<select id="hypernetwork_model" name="hypernetwork_model">
<!-- <option value="" selected>None</option> -->
</select>
<input id="hypernetwork_model" type="text" spellcheck="false" class="model-filter" data-path="" />
</td></tr>
<tr id="hypernetwork_strength_container" class="pl-5">
<td><label for="hypernetwork_strength_slider">Hypernetwork Strength:</label></td>
@ -427,6 +423,7 @@
<script src="media/js/image-modifiers.js"></script>
<script src="media/js/auto-save.js"></script>
<script src="media/js/searchable-models.js"></script>
<script src="media/js/main.js"></script>
<script src="media/js/themes.js"></script>
<script src="media/js/dnd.js"></script>

View File

@ -0,0 +1,92 @@
.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 6px 6px;
font-size: 9pt;
font-weight: bold;
}
.model-list li.model-file {
color: var(--input-text-color);
list-style: none;
padding-left: 12px;
padding-right:20px;
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);
}

View File

@ -93,6 +93,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
}
@ -102,6 +105,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
}

View File

@ -37,9 +37,9 @@ let gfpganModelField = document.querySelector("#gfpgan_model")
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')
@ -962,7 +962,7 @@ function getCurrentUserRequest() {
// allow_nsfw: allowNSFWField.checked,
vram_usage_level: vramUsageLevelField.value,
//render_device: undefined, // Set device affinity. Prefer this device, but wont activate.
use_stable_diffusion_model: stableDiffusionModelField.value,
use_stable_diffusion_ll: stableDiffusionModelField.value,
use_vae_model: vaeModelField.value,
stream_progress_updates: true,
stream_image_progress: (numOutputsTotal > 50 ? false : streamImageProgressField.checked),
@ -1315,79 +1315,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 gfpgan_model_key = "gfpgan_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 selectedGfpganModel = SETTINGS[gfpgan_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 = '<H1>🔥Malware alert!🔥</H1><h2>The file <i>' + models['scan-error'] + '</i> in your <tt>models/stable-diffusion</tt> folder is probably malware infected.</h2><h2>Please delete this file from the folder before proceeding!</h2>After deleting the file, reload this page.<br><br><button onClick="window.location.reload();">Reload Page</button>'
makeImageBtn.disabled = true
}
const stableDiffusionOptions = modelsOptions['stable-diffusion']
const vaeOptions = modelsOptions['vae']
const hypernetworkOptions = modelsOptions['hypernetwork']
const gfpganOptions = modelsOptions['gfpgan']
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 != "" ? "&nbsp;&nbsp;"+modelName : modelName) : 'None'
if (path + modelName === selectedModel) {
modelOption.selected = true
}
modelField.appendChild(modelOption)
} else {
// Since <optgroup/>s can't be nested, don't show empty groups
if (modelName[1].some(child => typeof(child) == 'string')) {
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))
gfpganOptions.forEach(createModelOptions(gfpganModelField,selectedGfpganModel))
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

View File

@ -0,0 +1,591 @@
"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.
<input id="stable_diffusion_model" type="text" spellcheck="false" class="model-filter" data-path="" />
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) {
// is there an immediate sibling?
let prevSibling = elem.previousElementSibling
if (prevSibling) {
// if the previous sibling is a model file, just select it
if (prevSibling.classList.contains('model-file')) return prevSibling
// if the previous sibling is a model folder, select the last model file it contains
if (prevSibling.classList.contains('model-folder')) return prevSibling.firstElementChild.lastElementChild
}
// no sibling model file and no sibling model folder. look for siblings around the parent element.
prevSibling = elem.parentElement.parentElement.previousElementSibling
if (prevSibling) {
// if the previous entry is a model file, select it
if (prevSibling.classList.contains('model-file')) return prevSibling
// is there another model folder to jump to before the current one?
if (prevSibling.classList.contains('model-folder')) return prevSibling.firstElementChild.lastElementChild
}
}
findNextSibling(elem) {
// is there an immediate sibling?
let nextSibling = elem.nextElementSibling
if (nextSibling) {
// if the next sibling is a model file, just select it
if (nextSibling.classList.contains('model-file')) return nextSibling
// if the next sibling is a model folder, select the first model file it contains
if (nextSibling.classList.contains('model-folder')) return nextSibling.firstElementChild.firstElementChild
}
// no sibling model file and no sibling model folder. look for siblings around the parent element.
nextSibling = elem.parentElement.parentElement.nextElementSibling
if (nextSibling) {
// if the next entry is a model file, select it
if (nextSibling.classList.contains('model-file')) return nextSibling
// is there another model folder to jump to after the current one?
if (nextSibling.classList.contains('model-folder')) return nextSibling.firstElementChild.firstElementChild
}
}
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.highlightedModelEntry.closest('.model-list').scrollTop = 0
}
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'))
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) {
// sort the models in alphabetical order, root folder models last
models.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
/*
// sort the models in alphabetical order, root folder models first
models = models.sort()
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().localeCompare(models[i+1].toLowerCase()) > 0) // same folder, sort by alphabetical order
|| (this.getFolder(models[i]).toLowerCase().localeCompare(this.getFolder(models[i+1]).toLowerCase()) > 0) // 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.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 = `<i id="${this.modelFilter.id}-model-filter-arrow" class="model-selector-arrow fa-solid fa-angle-down"></i>
<ul id="${this.modelFilter.id}-model-list" class="model-list">
<li id="${this.modelFilter.id}-model-no-result" class="model-no-result">No result</li>
<li id="${this.modelFilter.id}-model-result" class="model-result">
<ul>
`
if (this.noneEntry != '') {
html += `<li data-path='' class='model-file in-root-folder'>${this.noneEntry}</li>`
}
let currentFolder = ''
models.forEach(entry => {
const folder = entry.substring(0, 1) == '/' ? entry.substring(1, entry.lastIndexOf('/')) : ''
if (folder !== '' && folder !== currentFolder) {
if (currentFolder != '') {
html += '</ul></li>'
}
html += `<li class='model-folder'>/${folder}<ul>`
currentFolder = folder
}
else if (folder == '' && currentFolder !== '') {
currentFolder = ''
html += '</ul></li>'
}
const modelName = entry.substring(entry.lastIndexOf('/') + 1)
if (entry.substring(0, 1) == '/') {
entry = entry.substring(1)
}
html += `<li data-path='${entry}' class='model-file${currentFolder == '' ? ' in-root-folder' : ''}'>${modelName}</li>`
})
if (currentFolder != '') {
html += '</ul></li>'
}
html += `
</ul>
</li>
</ul>
`
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 = '<H1>🔥Malware alert!🔥</H1><h2>The file <i>' + modelsCache['scan-error'] + '</i> in your <tt>models/stable-diffusion</tt> folder is probably malware infected.</h2><h2>Please delete this file from the folder before proceeding!</h2>After deleting the file, reload this page.<br><br><button onClick="window.location.reload();">Reload Page</button>'
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)