Improved Directory Dropdowns.

- Each `DirectoryDropdown` now remembers the longest/most-recent path (navigating with keys).
- Separated mouse and keyboard directory dropdown selection.
- Clicking, scrolling and tapping no longer dismiss directory dropdowns.
- Added a small border on the left on the keyboard-highlighted dropdown selection.
- Fixed bug with dropdown scrolling with Model Info View.
- Encapsulated model directories formally in new `ModelDirectories` data structure.
- Moved download button in the Download Tab.
This commit is contained in:
Christian Bastian
2024-03-31 03:02:01 -04:00
parent bfcfb509ce
commit 683012a2e2
2 changed files with 452 additions and 252 deletions

View File

@@ -222,6 +222,7 @@
overflow-wrap: break-word;
overflow-y: auto;
padding: 20px;
position: relative;
}
.model-manager .model-info-container {
@@ -450,13 +451,13 @@
}
.model-manager .search-dropdown {
position: absolute;
background-color: var(--bg-color);
border: 2px var(--border-color) solid;
border-radius: 10px;
color: var(--fg-color);
max-height: 30vh;
overflow: auto;
border-radius: 10px;
position: absolute;
z-index: 1;
}
@@ -477,10 +478,15 @@
display: none; /* Safari and Chrome */
}
.model-manager .search-dropdown > p.search-dropdown-selected {
.model-manager .search-dropdown > p.search-dropdown-key-selected,
.model-manager .search-dropdown > p.search-dropdown-mouse-selected {
background-color: var(--border-color);
}
.model-manager .search-dropdown > p.search-dropdown-key-selected {
border-left: 1mm solid var(--input-text);
}
/* model manager settings */
.model-manager .model-manager-settings > div,
.model-manager .model-manager-settings > label {

View File

@@ -51,6 +51,30 @@ const IMAGE_EXTENSIONS = [
".preview.apng",
]; // TODO: /model-manager/image/extensions
/**
* @param {string} s
* @param {string} prefix
* @returns {string}
*/
function removePrefix(s, prefix) {
if (s.length >= prefix.length && s.startsWith(prefix)){
return s.substring(prefix.length);
}
return s;
}
/**
* @param {string} s
* @param {string} suffix
* @returns {string}
*/
function removeSuffix(s, suffix) {
if (s.length >= suffix.length && s.endsWith(suffix)){
return s.substring(0, s.length - suffix.length);
}
return s;
}
class SearchPath {
/**
* @param {string} path
@@ -621,13 +645,188 @@ class ImageSelect {
}
/**
* @typedef {Object} DirectoryItem
* @param {string} name
* @param {number | undefined} childCount
* @param {number | undefined} childIndex
* @typedef {Object} DirectoryItem
* @property {String} name
* @property {number | undefined} childCount
* @property {number | undefined} childIndex
*/
const DROPDOWN_DIRECTORY_SELECTION_CLASS = "search-dropdown-selected";
class ModelDirectories {
/** @type {DirectoryItem[]} */
data = [];
/**
* @returns {number}
*/
rootIndex() {
return 0;
}
/**
* @param {any} index
* @returns {boolean}
*/
isValidIndex(index) {
return typeof index === "number" && 0 <= index && index < this.data.length;
}
/**
* @param {number} index
* @returns {DirectoryItem}
*/
getItem(index) {
if (!this.isValidIndex(index)) {
throw new Error(`Index '${index}' is not valid!`);
}
return this.data[index];
}
/**
* @param {DirectoryItem | number} item
* @returns {boolean}
*/
isDirectory(item) {
if (typeof item === "number") {
item = this.getItem(item);
}
const childCount = item.childCount;
return childCount !== undefined && childCount != null;
}
/**
* @param {DirectoryItem | number} item
* @returns {boolean}
*/
isEmpty(item) {
if (typeof item === "number") {
item = this.getItem(item);
}
if (!this.isDirectory(item)) {
throw new Error("Item is not a directory!");
}
return item.childCount === 0;
}
/**
* Returns a slice of children from the directory list.
* @param {DirectoryItem | number} item
* @returns {DirectoryItem[]}
*/
getChildren(item) {
if (typeof item === "number") {
item = this.getItem(item);
if (!this.isDirectory(item)) {
throw new Error("Item is not a directory!");
}
}
else if (!this.isDirectory(item)) {
throw new Error("Item is not a directory!");
}
const count = item.childCount;
const index = item.childIndex;
return this.data.slice(index, index + count);
}
/**
* Returns index of child in parent directory. Returns -1 if DNE.
* @param {DirectoryItem | number} parent
* @param {string} name
* @returns {number}
*/
findChildIndex(parent, name) {
const item = this.getItem(parent);
if (!this.isDirectory(item)) {
throw new Error("Item is not a directory!");
}
const start = item.childIndex;
const children = this.getChildren(item);
const index = children.findIndex((item) => {
return item.name === name;
});
if (index === -1) {
return -1;
}
return index + start;
}
/**
* Returns a list of matching search results and valid path.
* @param {string} filter
* @param {string} searchSeparator
* @param {boolean} directoriesOnly
* @returns {[string[], string]}
*/
search(filter, searchSeparator, directoriesOnly) {
let cwd = this.rootIndex();
let indexLastWord = 1;
while (true) {
const indexNextWord = filter.indexOf(searchSeparator, indexLastWord);
if (indexNextWord === -1) {
// end of filter
break;
}
const item = this.getItem(cwd);
if (!this.isDirectory(item) || this.isEmpty(item)) {
break;
}
const word = filter.substring(indexLastWord, indexNextWord);
cwd = this.findChildIndex(cwd, word);
if (!this.isValidIndex(cwd)) {
return [[], ""];
}
indexLastWord = indexNextWord + 1;
}
//const cwdPath = filter.substring(0, indexLastWord);
const lastWord = filter.substring(indexLastWord);
const children = this.getChildren(cwd);
if (directoriesOnly) {
let indexPathEnd = indexLastWord;
const results = children.filter((child) => {
return (
this.isDirectory(child) &&
child.name.startsWith(lastWord)
);
}).map((directory) => {
const children = this.getChildren(directory);
const hasChildren = children.some((item) => {
return this.isDirectory(item);
});
const suffix = hasChildren ? searchSeparator : "";
//const suffix = searchSeparator;
if (directory.name == lastWord) {
indexPathEnd += searchSeparator.length + directory.name.length + 1;
}
return directory.name + suffix;
});
const path = filter.substring(0, indexPathEnd);
return [results, path];
}
else {
let indexPathEnd = indexLastWord;
const results = children.filter((child) => {
return child.name.startsWith(lastWord);
}).map((item) => {
const isDir = this.isDirectory(item);
const isNonEmptyDirectory = isDir && item.childCount > 0;
const suffix = isNonEmptyDirectory ? searchSeparator : "";
//const suffix = isDir ? searchSeparator : "";
if (!isDir && item.name == lastWord) {
indexPathEnd += searchSeparator.length + item.name.length + 1;
}
return item.name + suffix;
});
const path = filter.substring(0, indexPathEnd);
return [results, path];
}
}
}
const DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS = "search-dropdown-key-selected";
const DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS = "search-dropdown-mouse-selected";
class ModelData {
/** @type {string} */
@@ -639,10 +838,12 @@ class ModelData {
/** @type {Object} */
models = {};
/** @type {DirectoryItem[]} */
directories = [];
/** @type {ModelDirectories} */
directories = null;
constructor() {}
constructor() {
this.directories = new ModelDirectories();
}
}
class DirectoryDropdown {
@@ -667,12 +868,12 @@ class DirectoryDropdown {
/** @type {() => Promise<void>} */
#submitCallback = null;
/** @type {string} */
#currentPath = "/";
/** @type {string} */
#deepestPreviousPath = "/";
/** @type {Any} */
#touchSelectionStart = null;
/**
* @param {ModelData} modelData
* @param {HTMLInputElement} input
@@ -697,22 +898,28 @@ class DirectoryDropdown {
this.showDirectoriesOnly = showDirectoriesOnly;
input.addEventListener("input", () => {
this.#update();
const path = this.#updateOptions();
if (path !== undefined) {
this.#restoreSelectedOption(path);
this.#updateDeepestPath(path);
}
updateCallback();
});
input.addEventListener("focus", () => {
this.#update();
const path = this.#updateOptions();
if (path !== undefined) {
this.#deepestPreviousPath = path;
this.#restoreSelectedOption(path);
}
updateCallback();
});
input.addEventListener("blur", () => { dropdown.style.display = "none"; });
input.addEventListener(
"keydown",
(e) => {
input.addEventListener("keydown", async(e) => {
const options = dropdown.children;
let iSelection;
for (iSelection = 0; iSelection < options.length; iSelection++) {
const selection = options[iSelection];
if (selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_CLASS)) {
if (selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS)) {
break;
}
}
@@ -720,7 +927,7 @@ class DirectoryDropdown {
e.stopPropagation();
if (iSelection < options.length) {
const selection = options[iSelection];
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS);
}
else {
e.target.blur();
@@ -732,27 +939,31 @@ class DirectoryDropdown {
e.stopPropagation();
e.preventDefault(); // prevent cursor move
const input = e.target;
DirectoryDropdown.selectionToInput(input, selection, modelData.searchSeparator);
this.#update();
updateCallback();
//submitCallback();
/*
const options = dropdown.children;
if (options.length > 0) {
// arrow key navigation
options[0].classList.add(DROPDOWN_DIRECTORY_SELECTION_CLASS);
const searchSeparator = modelData.searchSeparator;
DirectoryDropdown.selectionToInput(
input,
selection,
searchSeparator,
DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS
);
const path = this.#updateOptions();
if (path !== undefined) {
this.#restoreSelectedOption(path);
this.#updateDeepestPath(path);
}
*/
updateCallback();
//await submitCallback();
}
}
else if (e.key === "ArrowLeft" && dropdown.style.display !== "none") {
const input = e.target;
const oldFilterText = input.value;
const iSep = oldFilterText.lastIndexOf(modelData.searchSeparator, oldFilterText.length - 2);
const searchSeparator = modelData.searchSeparator;
const iSep = oldFilterText.lastIndexOf(searchSeparator, oldFilterText.length - 2);
const newFilterText = oldFilterText.substring(0, iSep + 1);
if (oldFilterText !== newFilterText) {
const delta = oldFilterText.substring(iSep + 1);
let isMatch = delta[delta.length-1] === modelData.searchSeparator;
let isMatch = delta[delta.length-1] === searchSeparator;
if (!isMatch) {
const options = dropdown.children;
for (let i = 0; i < options.length; i++) {
@@ -767,28 +978,13 @@ class DirectoryDropdown {
e.stopPropagation();
e.preventDefault(); // prevent cursor move
input.value = newFilterText;
this.#update();
const path = this.#updateOptions();
if (path !== undefined) {
this.#restoreSelectedOption(path);
this.#updateDeepestPath(path);
}
updateCallback();
//submitCallback();
/*
const options = dropdown.children;
let isSelected = false;
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (option.innerText.startsWith(delta)) {
option.classList.add(DROPDOWN_DIRECTORY_SELECTION_CLASS);
isSelected = true;
break;
}
}
if (!isSelected) {
const options = dropdown.children;
if (options.length > 0) {
// arrow key navigation
options[0].classList.add(DROPDOWN_DIRECTORY_SELECTION_CLASS);
}
}
*/
//await submitCallback();
}
}
}
@@ -797,11 +993,19 @@ class DirectoryDropdown {
const input = e.target
const selection = options[iSelection];
if (selection !== undefined && selection !== null) {
DirectoryDropdown.selectionToInput(input, selection, modelData.searchSeparator);
this.#update();
DirectoryDropdown.selectionToInput(
input,
selection,
modelData.searchSeparator,
DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS
);
const path = this.#updateOptions();
if (path !== undefined) {
this.#updateDeepestPath(path);
}
updateCallback();
}
submitCallback();
await submitCallback();
input.blur();
}
else if ((e.key === "ArrowDown" || e.key === "ArrowUp") && dropdown.style.display !== "none") {
@@ -810,36 +1014,33 @@ class DirectoryDropdown {
let iNext = options.length;
if (iSelection < options.length) {
const selection = options[iSelection];
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS);
const delta = e.key === "ArrowDown" ? 1 : -1;
iNext = iSelection + delta;
if (0 <= iNext && iNext < options.length) {
const selectionNext = options[iNext];
selectionNext.classList.add(DROPDOWN_DIRECTORY_SELECTION_CLASS);
if (iNext < 0) {
iNext = options.length - 1;
}
else if (iNext >= options.length) {
iNext = 0;
}
const selectionNext = options[iNext];
selectionNext.classList.add(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS);
}
else if (iSelection === options.length) {
else if (iSelection === options.length) { // none
iNext = e.key === "ArrowDown" ? 0 : options.length-1;
const selection = options[iNext]
selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_CLASS);
const selection = options[iNext];
selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS);
}
if (0 <= iNext && iNext < options.length) {
let dropdownTop = dropdown.scrollTop;
const dropdownHeight = dropdown.offsetHeight;
const selection = options[iNext];
const selectionHeight = selection.offsetHeight;
const selectionTop = selection.offsetTop;
dropdownTop = Math.max(dropdownTop, selectionTop - dropdownHeight + selectionHeight);
dropdownTop = Math.min(dropdownTop, selectionTop);
dropdown.scrollTop = dropdownTop;
DirectoryDropdown.#clampDropdownScrollTop(dropdown, options[iNext]);
}
else {
dropdown.scrollTop = 0;
const options = dropdown.children;
for (iSelection = 0; iSelection < options.length; iSelection++) {
const selection = options[iSelection];
if (selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_CLASS)) {
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
if (selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS)) {
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS);
}
}
}
@@ -852,173 +1053,161 @@ class DirectoryDropdown {
* @param {HTMLInputElement} input
* @param {HTMLParagraphElement | undefined | null} selection
* @param {String} searchSeparator
* * @param {String} className
*/
static selectionToInput(input, selection, searchSeparator) {
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
static selectionToInput(input, selection, searchSeparator, className) {
selection.classList.remove(className);
const selectedText = selection.innerText;
const oldFilterText = input.value;
const iSep = oldFilterText.lastIndexOf(searchSeparator);
const previousPath = oldFilterText.substring(0, iSep + 1);
input.value = previousPath + selectedText;
}
#update() {
// TODO: create a wrapper around ModelData.directories to make access easier
const directories = this.#modelData.directories;
/**
* @param {string} path
*/
#updateDeepestPath = (path) => {
const deepestPath = this.#deepestPreviousPath;
if (path.length > deepestPath.length || !deepestPath.startsWith(path)) {
this.#deepestPreviousPath = path;
}
};
/**
* @param {HTMLDivElement} dropdown
* @param {HTMLParagraphElement} selection
*/
static #clampDropdownScrollTop = (dropdown, selection) => {
let dropdownTop = dropdown.scrollTop;
const dropdownHeight = dropdown.offsetHeight;
const selectionHeight = selection.offsetHeight;
const selectionTop = selection.offsetTop;
dropdownTop = Math.max(dropdownTop, selectionTop - dropdownHeight + selectionHeight);
dropdownTop = Math.min(dropdownTop, selectionTop);
dropdown.scrollTop = dropdownTop;
};
/**
* @param {string} path
*/
#restoreSelectedOption(path) {
const searchSeparator = this.#modelData.searchSeparator;
const deepest = this.#deepestPreviousPath;
if (deepest.length >= path.length && deepest.startsWith(path)) {
let name = deepest.substring(path.length);
name = removePrefix(name, searchSeparator);
const i1 = name.indexOf(searchSeparator);
if (i1 !== -1) {
name = name.substring(0, i1);
}
const dropdown = this.element;
const options = dropdown.children;
let iSelection;
for (iSelection = 0; iSelection < options.length; iSelection++) {
const selection = options[iSelection];
let text = removeSuffix(selection.innerText, searchSeparator);
if (text === name) {
selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_KEY_CLASS);
dropdown.scrollTop = dropdown.scrollHeight; // snap to top
DirectoryDropdown.#clampDropdownScrollTop(dropdown, selection);
break;
}
}
if (iSelection === options.length) {
dropdown.scrollTop = 0;
}
}
}
/**
* Returns path if update was successful.
* @returns {string | undefined}
*/
#updateOptions() {
const dropdown = this.element;
const input = this.#input;
const modelType = this.#getModelType();
const updateCallback = this.#updateCallback;
const submitCallback = this.#submitCallback;
const showDirectoriesOnly = this.showDirectoriesOnly;
const searchSeparator = this.#modelData.searchSeparator;
const filter = input.value;
if (filter[0] !== searchSeparator) {
dropdown.style.display = "none";
return;
return undefined;
}
let cwd = 0;
if (modelType !== "") {
const root = directories[0];
const rootChildIndex = root["childIndex"];
const rootChildCount = root["childCount"];
cwd = null;
for (let i = rootChildIndex; i < rootChildIndex + rootChildCount; i++) {
const modelDir = directories[i];
if (modelDir["name"] === modelType) {
cwd = i;
break;
}
}
}
// TODO: directories === undefined?
let indexLastWord = 1;
while (true) {
const indexNextWord = filter.indexOf(searchSeparator, indexLastWord);
if (indexNextWord === -1) {
// end of filter
break;
}
const item = directories[cwd];
const childCount = item["childCount"];
if (childCount === undefined) {
// file
break;
}
if (childCount === 0) {
// directory is empty
break;
}
const childIndex = item["childIndex"];
const items = directories.slice(childIndex, childIndex + childCount);
const word = filter.substring(indexLastWord, indexNextWord);
cwd = null;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const itemName = items[itemIndex]["name"];
if (itemName === word) {
// directory exists
cwd = childIndex + itemIndex;
break;
}
}
if (cwd === null) {
// directory does not exist
break;
}
indexLastWord = indexNextWord + 1;
}
if (cwd === null) {
dropdown.style.display = "none";
return;
}
let options = [];
let indexPathEnd = indexLastWord;
{
const lastWord = filter.substring(indexLastWord);
const item = directories[cwd];
if (item["childIndex"] === undefined) {
dropdown.style.display = "none";
return;
}
const childIndex = item["childIndex"];
const childCount = item["childCount"];
const children = directories.slice(childIndex, childIndex + childCount);
for (let i = 0; i < children.length; i++) {
const child = children[i];
const grandChildCount = child["childCount"];
const isDir = grandChildCount !== undefined && grandChildCount !== null;
const itemName = child["name"];
if (showDirectoriesOnly) {
if (isDir && itemName.startsWith(lastWord)) {
let existsDirectoryGrandchild = false;
const grandChildIndex = child["childIndex"];
const grandChildren = directories.slice(grandChildIndex, grandChildIndex + grandChildCount);
for (let j = 0; j < grandChildren.length; j++) {
const grandChild = grandChildren[j];
const greatGrandChildCount = grandChild["childCount"];
if (greatGrandChildCount !== undefined && greatGrandChildCount !== null) {
existsDirectoryGrandchild = true;
break;
}
}
options.push(itemName + (existsDirectoryGrandchild ? searchSeparator : ""));
}
}
else {
if (itemName.startsWith(lastWord)) {
options.push(itemName + (isDir && grandChildCount > 0 ? searchSeparator : ""));
}
if (!isDir && itemName == lastWord) {
indexPathEnd += searchSeparator.length + itemName.length + 1;
}
}
}
}
const modelType = this.#getModelType();
const searchPrefix = modelType !== "" ? searchSeparator + modelType : "";
const directories = this.#modelData.directories;
const [options, path] = directories.search(
searchPrefix + filter,
searchSeparator,
this.showDirectoriesOnly,
);
if (options.length === 0) {
dropdown.style.display = "none";
return;
}
const path = filter.substring(0, indexPathEnd);
this.#currentPath = path;
if (!this.#deepestPreviousPath.startsWith(path)) {
this.#deepestPreviousPath = path;
return undefined;
}
const selection_select = (e) => {
const selection = e.target;
if (e.movementX === 0 && e.movementY === 0) { return; }
if (!selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_CLASS)) {
if (!selection.classList.contains(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS)) {
// assumes only one will ever selected at a time
e.stopPropagation();
const children = dropdown.children;
let iChild;
for (iChild = 0; iChild < children.length; iChild++) {
for (let iChild = 0; iChild < children.length; iChild++) {
const child = children[iChild];
child.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
child.classList.remove(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS);
}
selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_CLASS);
selection.classList.add(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS);
}
};
const selection_deselect = (e) => {
e.stopPropagation();
e.target.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
e.target.classList.remove(DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS);
};
const selection_submit = (e) => {
const selection_submit = async(e) => {
e.stopPropagation();
e.preventDefault();
const selection = e.target;
DirectoryDropdown.selectionToInput(input, selection, searchSeparator);
this.#update();
updateCallback();
submitCallback();
DirectoryDropdown.selectionToInput(
input,
selection,
searchSeparator,
DROPDOWN_DIRECTORY_SELECTION_MOUSE_CLASS
);
const path = this.#updateOptions(); // TODO: is this needed?
if (path !== undefined) {
this.#updateDeepestPath(path);
}
this.#updateCallback();
};
const innerHtml = options.map((text) => {
const selection_touch = async(e) => {
const [startX, startY] = this.#touchSelectionStart;
const [endX, endY] = [
e.changedTouches[0].clientX,
e.changedTouches[0].clientY
];
if (startX === endX && startY === endY) {
const touch = e.changedTouches[0];
const box = dropdown.getBoundingClientRect();
if (touch.clientX >= box.left &&
touch.clientX <= box.right &&
touch.clientY >= box.top &&
touch.clientY <= box.bottom) {
selection_submit(e);
}
}
};
const touch_start = (e) => {
this.#touchSelectionStart = [
e.changedTouches[0].clientX,
e.changedTouches[0].clientY
];
};
dropdown.innerHTML = "";
dropdown.append.apply(dropdown, options.map((text) => {
/** @type {HTMLParagraphElement} */
const p = $el(
"p",
@@ -1027,21 +1216,24 @@ class DirectoryDropdown {
onmousemove: (e) => selection_select(e),
onmouseleave: (e) => selection_deselect(e),
onmousedown: (e) => selection_submit(e),
ontouchstart: (e) => touch_start(e),
ontouchmove: (e) => touch_move(e),
ontouchend: (e) => selection_touch(e),
},
[
text
]
);
return p;
});
dropdown.innerHTML = "";
dropdown.append.apply(dropdown, innerHtml);
}));
// TODO: handle when dropdown is near the bottom of the window
const inputRect = input.getBoundingClientRect();
dropdown.style.width = inputRect.width + "px";
dropdown.style.top = (input.offsetTop + inputRect.height) + "px";
dropdown.style.left = input.offsetLeft + "px";
dropdown.style.display = "block";
return path;
}
}
@@ -1726,9 +1918,11 @@ class ModelInfoView {
const fileDirectory = info["File Directory"];
if (fileDirectory !== undefined && fileDirectory !== null && fileDirectory !== "") {
this.elements.moveDestinationInput.placeholder = fileDirectory
this.elements.moveDestinationInput.value = fileDirectory; // TODO: noise vs convenience
}
else {
this.elements.moveDestinationInput.placeholder = searchSeparator;
this.elements.moveDestinationInput.value = searchSeparator;
}
@@ -2214,54 +2408,54 @@ class DownloadTab {
$el("div", {
style: { "margin-top": "8px" }
}, [
$el("button.icon-button", {
textContent: "📥︎",
onclick: async (e) => {
const formData = new FormData();
formData.append("download", info["downloadUrl"]);
formData.append("path", el_saveDirectoryPath.value);
formData.append("name", (() => {
const filename = info["fileName"];
const name = el_filename.value;
if (name === "") {
return filename;
}
const ext = MODEL_EXTENSIONS.find((ext) => {
return filename.endsWith(ext);
}) ?? "";
return name + ext;
})());
const image = downloadPreviewSelect.getImage();
formData.append("image", image === imageUri() ? "" : image);
formData.append("overwrite", this.elements.overwrite.checked);
e.target.disabled = true;
const [success, resultText] = await request(
"/model-manager/model/download",
{
method: "POST",
body: formData,
}
).then((data) => {
const success = data["success"];
if (!success) {
console.warn(data["invalid"]);
}
return [success, success ? "✔" : "📥︎"];
}).catch((err) => {
return [false, "📥︎"];
});
if (success) {
this.#updateModels();
}
buttonAlert(e.target, success, "✔", "✖", resultText);
e.target.disabled = success;
},
}),
$el("div.row.tab-header-flex-block", [
el_saveDirectoryPath,
searchDropdown.element,
]),
$el("div.row.tab-header-flex-block", [
$el("button.icon-button", {
textContent: "📥︎",
onclick: async (e) => {
const formData = new FormData();
formData.append("download", info["downloadUrl"]);
formData.append("path", el_saveDirectoryPath.value);
formData.append("name", (() => {
const filename = info["fileName"];
const name = el_filename.value;
if (name === "") {
return filename;
}
const ext = MODEL_EXTENSIONS.find((ext) => {
return filename.endsWith(ext);
}) ?? "";
return name + ext;
})());
const image = downloadPreviewSelect.getImage();
formData.append("image", image === imageUri() ? "" : image);
formData.append("overwrite", this.elements.overwrite.checked);
e.target.disabled = true;
const [success, resultText] = await request(
"/model-manager/model/download",
{
method: "POST",
body: formData,
}
).then((data) => {
const success = data["success"];
if (!success) {
console.warn(data["invalid"]);
}
return [success, success ? "✔" : "📥︎"];
}).catch((err) => {
return [false, "📥︎"];
});
if (success) {
this.#updateModels();
}
buttonAlert(e.target, success, "✔", "✖", resultText);
e.target.disabled = success;
},
}),
el_filename,
]),
downloadPreviewSelect.elements.radioGroup,
@@ -2944,7 +3138,7 @@ class ModelManager extends ComfyDialog {
const newModels = await request("/model-manager/models/list");
Object.assign(modelData.models, newModels); // NOTE: do NOT create a new object
const newModelDirectories = await request("/model-manager/models/directory-list");
modelData.directories.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array
modelData.directories.data.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array
this.#modelTab.updateModelGrid();
}