Top bar reworked.

- Now should work properly even on narrow (mobile) screens.
- Separated close Model Info View and Close Model Manager buttons.
This commit is contained in:
Christian Bastian
2024-04-05 00:26:13 -04:00
parent 2e0b99a2a4
commit c0375e4f4c
2 changed files with 175 additions and 119 deletions

View File

@@ -5,7 +5,7 @@
height: 100%; height: 100%;
max-width: unset; max-width: unset;
max-height: unset; max-height: unset;
padding: 10px; padding: 8px;
color: var(--bg-color); color: var(--bg-color);
z-index: 2000; z-index: 2000;
} }
@@ -153,61 +153,57 @@
/* sidebar buttons */ /* sidebar buttons */
.model-manager .sidebar-buttons { .model-manager .sidebar-buttons {
overflow: hidden; overflow: hidden;
padding-right: 10px;
color: var(--input-text); color: var(--input-text);
} }
/* tabs */ /* main content */
.model-manager .comfy-tabs { .model-manager .model-manager-panel {
color: var(--fg-color); color: var(--fg-color);
} }
.model-manager .comfy-tabs-head { .model-manager .model-manager-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
border-bottom: 2px solid var(--border-color); border-bottom: 2px solid var(--border-color);
display: flex;
gap: 4px;
} }
.model-manager .comfy-tabs-head .head-item { .model-manager .model-manager-tabs .head-item {
padding: 8px 12px; background-color: var(--comfy-menu-bg);
border: 2px solid var(--border-color); border: 2px solid var(--border-color);
border-bottom: none; border-bottom: none;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-top-right-radius: 8px; border-top-right-radius: 8px;
background-color: var(--comfy-menu-bg);
cursor: pointer; cursor: pointer;
padding: 8px 12px;
margin-bottom: 0px; margin-bottom: 0px;
z-index: 1; z-index: 1;
} }
.model-manager .comfy-tabs-head .head-item.active { .model-manager .model-manager-tabs .head-item.active {
background-color: var(--comfy-input-bg); background-color: var(--comfy-input-bg);
cursor: default; cursor: default;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.model-manager .comfy-tabs-body { .model-manager .model-manager-body {
background-color: var(--bg-color); background-color: var(--bg-color);
border: 2px solid var(--border-color);
border-top: none;
padding: 16px 0px; padding: 16px 0px;
} }
.model-manager .comfy-tabs { .model-manager .model-manager-panel {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.model-manager .comfy-tabs-body { .model-manager .model-manager-body {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.model-manager .comfy-tabs-body > div { .model-manager .model-manager-body > div {
position: relative; position: relative;
height: 100%; height: 100%;
width: auto; width: auto;
@@ -218,12 +214,9 @@
/* model info view */ /* model info view */
.model-manager .model-info-view { .model-manager .model-info-view {
background-color: var(--bg-color); background-color: var(--bg-color);
border: 2px solid var(--border-color);
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
margin-top: 40px;
overflow-wrap: break-word; overflow-wrap: break-word;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
@@ -446,23 +439,33 @@
/* topbar */ /* topbar */
.model-manager .topbar-buttons { .model-manager .topbar-buttons {
position: absolute;
display: flex; display: flex;
top: 10px; float: right;
right: 10px;
} }
.model-manager .topbar-buttons button { .model-manager .topbar-buttons button {
width: 33px;
height: 33px; height: 33px;
padding: 1px 6px; padding: 1px 6px;
width: 33px;
}
.model-manager .model-manager-head .topbar-left {
display: flex;
float: left;
}
.model-manager .model-manager-head .topbar-right {
column-gap: 4px;
display: flex;
flex-direction: row-reverse;
float: right;
} }
/* search dropdown */ /* search dropdown */
.model-manager .search-models { .model-manager .search-models {
display: flex; display: flex;
flex-direction: row;
flex: 1; flex: 1;
flex-direction: row;
min-width: 0; min-width: 0;
} }

View File

@@ -1508,10 +1508,10 @@ class ModelGrid {
* @param {Object.<HTMLInputElement>} settingsElements * @param {Object.<HTMLInputElement>} settingsElements
* @param {String} searchSeparator * @param {String} searchSeparator
* @param {String} systemSeparator * @param {String} systemSeparator
* @param {(searchPath: string) => Promise<void>} modelInfoCallback * @param {(searchPath: string) => Promise<void>} showModelInfo
* @returns {HTMLElement[]} * @returns {HTMLElement[]}
*/ */
static #generateInnerHtml(models, modelType, settingsElements, searchSeparator, systemSeparator, modelInfoCallback) { static #generateInnerHtml(models, modelType, settingsElements, searchSeparator, systemSeparator, showModelInfo) {
// TODO: separate text and model logic; getting too messy // TODO: separate text and model logic; getting too messy
// TODO: fallback on button failure to copy text? // TODO: fallback on button failure to copy text?
const canShowButtons = modelNodeType[modelType] !== undefined; const canShowButtons = modelNodeType[modelType] !== undefined;
@@ -1591,7 +1591,7 @@ class ModelGrid {
$el("button.icon-button.model-button", { $el("button.icon-button.model-button", {
type: "button", type: "button",
textContent: "ⓘ", textContent: "ⓘ",
onclick: async() => modelInfoCallback(searchPath), onclick: async() => { await showModelInfo(searchPath) },
draggable: false, draggable: false,
}), }),
]), ]),
@@ -1618,9 +1618,9 @@ class ModelGrid {
* @param {boolean} reverseSort * @param {boolean} reverseSort
* @param {Array} previousModelFilters * @param {Array} previousModelFilters
* @param {HTMLInputElement} modelFilter * @param {HTMLInputElement} modelFilter
* @param {(searchPath: string) => Promise<void>} modelInfoCallback * @param {(searchPath: string) => Promise<void>} showModelInfo
*/ */
static update(modelGrid, modelData, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter, modelInfoCallback) { static update(modelGrid, modelData, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter, showModelInfo) {
const models = modelData.models; const models = modelData.models;
let modelType = modelSelect.value; let modelType = modelSelect.value;
if (models[modelType] === undefined) { if (models[modelType] === undefined) {
@@ -1661,7 +1661,7 @@ class ModelGrid {
settings, settings,
modelData.searchSeparator, modelData.searchSeparator,
modelData.systemSeparator, modelData.systemSeparator,
modelInfoCallback, showModelInfo,
); );
modelGrid.append.apply(modelGrid, modelGridModels); modelGrid.append.apply(modelGrid, modelGridModels);
} }
@@ -1873,36 +1873,58 @@ class ModelInfoView {
/** @returns {void} */ /** @returns {void} */
show() { show() {
this.element.removeAttribute("style"); this.element.style = "";
this.element.scrollTop = 0;
} }
/** @returns {Promise<void>} */ /**
async hide() { * @param {boolean}
const notes = this.elements.notes; * @returns {Promise<boolean>}
if (notes !== undefined && notes !== null) { */
const noteValue = this.elements.notes.value; async trySave(promptUser) {
const savedNotesValue = this.#savedNotesValue; const noteValue = this.elements.notes.value;
if (noteValue.trim() !== savedNotesValue.trim()) { const savedNotesValue = this.#savedNotesValue;
const saveChanges = window.confirm("Save notes?"); if (noteValue.trim() === savedNotesValue.trim()) {
if (saveChanges) { return true;
const path = this.elements.info.dataset.path; }
const saved = await saveNotes(path, noteValue); const saveChanges = !promptUser || window.confirm("Save notes?");
if (!saved) { if (saveChanges) {
window.alert("Failed to save notes!"); const path = this.elements.info.dataset.path;
return; const saved = await saveNotes(path, noteValue);
} if (!saved) {
this.#savedNotesValue = ""; window.alert("Failed to save notes!");
} return false;
else { }
const discardChanges = window.confirm("Discard changes?"); this.#savedNotesValue = noteValue;
if (!discardChanges) { }
return; else {
} const discardChanges = window.confirm("Discard changes?");
} if (!discardChanges) {
return false;
}
else {
this.elements.notes.value = savedNotesValue;
} }
} }
return true;
}
/**
* @param {boolean?} promptSave
* @returns {Promise<boolean>}
*/
async tryHide(promptSave = true) {
const notes = this.elements.notes;
if (promptSave && notes !== undefined && notes !== null) {
const saved = await this.trySave(promptSave);
if (!saved) {
return false;
}
this.#savedNotesValue = "";
this.elements.notes.value = "";
}
this.element.style.display = "none"; this.element.style.display = "none";
return true;
} }
/** /**
@@ -2061,12 +2083,7 @@ class ModelInfoView {
elements.push($el("button", { elements.push($el("button", {
textContent: "Save Notes", textContent: "Save Notes",
onclick: async (e) => { onclick: async (e) => {
const path = this.elements.info.dataset.path; const saved = await this.trySave(false);
const newValue = notes.value;
const saved = await saveNotes(path, newValue);
if (saved) {
this.#savedNotesValue = newValue;
}
buttonAlert(e.target, saved); buttonAlert(e.target, saved);
}, },
})); }));
@@ -2802,9 +2819,6 @@ class ModelTab {
/** @type {ModelData} */ /** @type {ModelData} */
#modelData = null; #modelData = null;
/** @type {ModelInfoView} */
#modelInfoView = null;
/** @type {@param {() => Promise<void>}} */ /** @type {@param {() => Promise<void>}} */
#updateModels = null; #updateModels = null;
@@ -2817,17 +2831,16 @@ class ModelTab {
/** /**
* @param {() => Promise<void>} updateModels * @param {() => Promise<void>} updateModels
* @param {ModelData} modelData * @param {ModelData} modelData
* @param {ModelInfoView} modelInfoView * @param {(searchPath: string) => Promise<void>} showModelInfo
* @param {any} settingsElements * @param {any} settingsElements
*/ */
constructor(updateModels, modelData, modelInfoView, settingsElements) { constructor(updateModels, modelData, showModelInfo, settingsElements) {
/** @type {HTMLDivElement} */ /** @type {HTMLDivElement} */
const modelGrid = $el("div.comfy-grid"); const modelGrid = $el("div.comfy-grid");
this.elements.modelGrid = modelGrid; this.elements.modelGrid = modelGrid;
this.#updateModels = updateModels; this.#updateModels = updateModels;
this.#modelData = modelData; this.#modelData = modelData;
this.#modelInfoView = modelInfoView;
this.#settingsElements = settingsElements; this.#settingsElements = settingsElements;
const searchInput = $el("input.search-text-area", { const searchInput = $el("input.search-text-area", {
@@ -2844,19 +2857,6 @@ class ModelTab {
this.previousModelFilters[modelType] = value; this.previousModelFilters[modelType] = value;
}; };
/**
* @param {string} searchPath
*/
const showModelInfoView = async(searchPath) => {
this.#modelInfoView.update(
searchPath,
this.#updateModels,
this.#modelData.searchSeparator
).then(() => {
this.#modelInfoView.show();
});
}
const updateModelGrid = () => { const updateModelGrid = () => {
const sortValue = this.elements.modelSortSelect.value; const sortValue = this.elements.modelSortSelect.value;
const reverseSort = sortValue[0] === "-"; const reverseSort = sortValue[0] === "-";
@@ -2871,7 +2871,7 @@ class ModelTab {
reverseSort, reverseSort,
this.previousModelFilters, this.previousModelFilters,
this.elements.modelContentFilter, this.elements.modelContentFilter,
showModelInfoView, showModelInfo,
); );
this.element.parentElement.scrollTop = 0; this.element.parentElement.scrollTop = 0;
} }
@@ -3242,8 +3242,14 @@ class ModelManager extends ComfyDialog {
/** @type {SettingsTab} */ /** @type {SettingsTab} */
#settingsTab = null; #settingsTab = null;
/** @type {SidebarButtons} */ /** @type {HTMLDivElement} */
#sidebarButtons = null; #tabs = null;
/** @type {HTMLDivElement} */
#tabContents = null;
/** @type {HTMLButtonElement} */
#closeModelInfoButton = null;
constructor() { constructor() {
super(); super();
@@ -3261,10 +3267,29 @@ class ModelManager extends ComfyDialog {
); );
this.#settingsTab = settingsTab; this.#settingsTab = settingsTab;
const ACTIVE_TAB_CLASS = "active";
/**
* @param {searchPath: string}
* @return {Promise<void>}
*/
const showModelInfo = async(searchPath) => {
await this.#modelInfoView.update(
searchPath,
this.#refreshModels,
this.#modelData.searchSeparator
).then(() => {
this.#tabs.style.display = "none";
this.#tabContents.style.display = "none";
this.#closeModelInfoButton.style.display = "";
this.#modelInfoView.show();
});
}
const modelTab = new ModelTab( const modelTab = new ModelTab(
this.#refreshModels, this.#refreshModels,
this.#modelData, this.#modelData,
this.#modelInfoView, showModelInfo,
this.#settingsTab.elements.settings, // TODO: decouple settingsData from elements? this.#settingsTab.elements.settings, // TODO: decouple settingsData from elements?
); );
this.#modelTab = modelTab; this.#modelTab = modelTab;
@@ -3277,7 +3302,6 @@ class ModelManager extends ComfyDialog {
this.#downloadTab = downloadTab; this.#downloadTab = downloadTab;
const sidebarButtons = new SidebarButtons(this); const sidebarButtons = new SidebarButtons(this);
this.#sidebarButtons = sidebarButtons;
/** @type {Record<string, HTMLDivElement>} */ /** @type {Record<string, HTMLDivElement>} */
const head = {}; const head = {};
@@ -3295,11 +3319,8 @@ class ModelManager extends ComfyDialog {
const tabs = contents.map((content) => { const tabs = contents.map((content) => {
const name = content.getAttribute("data-name"); const name = content.getAttribute("data-name");
/** @type {HTMLDivElement} */ /** @type {HTMLDivElement} */
const tab = $el( const tab = $el("div.head-item", {
"div.head-item",
{
onclick: () => { onclick: () => {
const ACTIVE_TAB_CLASS = "active";
Object.keys(head).forEach((key) => { Object.keys(head).forEach((key) => {
if (name === key) { if (name === key) {
head[key].classList.add(ACTIVE_TAB_CLASS); head[key].classList.add(ACTIVE_TAB_CLASS);
@@ -3311,9 +3332,7 @@ class ModelManager extends ComfyDialog {
}); });
}, },
}, },
[ [name],
name,
],
); );
head[name] = tab; head[name] = tab;
body[name] = content; body[name] = content;
@@ -3321,50 +3340,67 @@ class ModelManager extends ComfyDialog {
}); });
tabs[0]?.click(); tabs[0]?.click();
this.element = $el( const closeManagerButton = $el("button.icon-button", {
textContent: "✖",
onclick: async() => {
const saved = await modelInfoView.trySave(true);
if (saved) {
this.close();
}
}
});
const closeModelInfoButton = $el("button.icon-button", {
$: (el) => (this.#closeModelInfoButton = el),
style: { display: "none" },
textContent: "⬅",
onclick: async() => { await this.#tryHideModelInfo(true); },
});
const modelManager = $el(
"div.comfy-modal.model-manager", "div.comfy-modal.model-manager",
{ {
$: (el) => (this.element = el),
parent: document.body, parent: document.body,
}, },
[ [
$el("div.comfy-modal-content", [ // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left $el("div.comfy-modal-content", [ // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left
modelInfoView.element, $el("div.model-manager-panel", [
$el("div.topbar-buttons", $el("div.model-manager-head", [
[ $el("div.topbar-right", [
sidebarButtons.element, closeManagerButton,
$el("button.icon-button", { closeModelInfoButton,
textContent: "✖", sidebarButtons.element,
onclick: async() => { ]),
if (modelInfoView.isVisible()) { // TODO: decouple back and close $el("div.topbar-left", [
this.close(); $el("div.model-manager-tabs", {
} $: (el) => (this.#tabs = el),
else { }, tabs),
await modelInfoView.hide(); ]),
} ]),
}, $el("div.model-manager-body", [
}), $el("div.model-manager-tab-contents", {
] $: (el) => (this.#tabContents = el),
), }, contents),
$el("div.comfy-tabs", [ modelInfoView.element,
$el("div.comfy-tabs-head", tabs), ]),
$el("div.comfy-tabs-body", contents),
]), ]),
]), ]),
] ]
); );
new ResizeObserver(() => { new ResizeObserver(() => {
if (this.element.style.display === "none") { if (modelManager.style.display === "none") {
return; return;
} }
const minWidth = 768; // magic value (could easily break) const minWidth = 768; // magic value (could easily break)
const managerRect = this.element.getBoundingClientRect(); const managerRect = modelManager.getBoundingClientRect();
const isNarrow = managerRect.width < minWidth; const isNarrow = managerRect.width < minWidth;
let texts = isNarrow ? ["⬇️", "📁", "⚙️"] : ["Download", "Models", "Settings"]; let texts = isNarrow ? ["⬇️", "📁", "⚙️"] : ["Download", "Models", "Settings"]; // magic values
texts.forEach((text, i) => { texts.forEach((text, i) => {
tabs[i].innerText = text; tabs[i].innerText = text;
}); });
}).observe(this.element); }).observe(modelManager);
this.#init(); this.#init();
} }
@@ -3383,6 +3419,23 @@ class ModelManager extends ComfyDialog {
modelData.directories.data.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(); this.#modelTab.updateModelGrid();
await this.#tryHideModelInfo(false);
}
/**
* @param {boolean} promptSave
* @returns {Promise<boolean>}
*/
#tryHideModelInfo = async(promptSave) => {
if (this.#tabContents.style.display === "none") {
if (!await this.#modelInfoView.tryHide(promptSave)) {
return false;
}
this.#closeModelInfoButton.style.display = "none";
this.#tabs.style.display = "";
this.#tabContents.style.display = "";
}
return true;
} }
} }