v1.2.0 Side Bar & Menu rework (#189)

* Basic side tool bar skeleton + Theme toggle (#164)

* Side bar skeleton

* Fix grid layout

* nit

* Add theme toggle logic

* Change primevue color theme to blue to match beta menu UI

* Add litegraph canvas splitter overlay (#177)

* Add vue wrapper

* Splitter overlay

* Move teleport to side bar comp

* Toolbar placeholder

* Move settings button from top menu to side bar (#178)

* Reverse relationship between splitter overlay and sidebar component (#180)

* Reverse relationship between splitter overlay and sidebar component

* nit

* Remove border on splitter

* Fix canvas shift (#186)

* Move queue/history display to side bar (#185)

* Side bar placeholder

* Pinia store for queue items

* Flatten task item

* Fix schema

* computed

* Switch running / pending order

* Use class-transformer

* nit

* Show display status

* Add tag severity style

* Add execution time

* nit

* Rename to execution success

* Add time display

* Sort queue desc order

* nit

* Add remove item feature

* Load workflow

* Add confirmation popup

* Add empty table placeholder

* Remove beta menu UI's queue button/list

* Add tests on litegraph widget text truncate (#191)

* Add tests on litegraph widget text truncate

* Updated screenshots

* Revert port change

* Remove screenshots

* Update test expectations [skip ci]

* Add back menu.settingsGroup for compatibility (#192)

* Close side bar on menu location set as disabled (#194)

* Remove placeholder side bar tabs (#196)

---------

Co-authored-by: bymyself <abolkonsky.rem@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2024-07-22 10:15:41 -04:00
committed by GitHub
parent 1521cd47c8
commit 65740a30c5
27 changed files with 938 additions and 349 deletions

View File

@@ -184,6 +184,11 @@ class ComfyApi extends EventTarget {
new CustomEvent("execution_start", { detail: msg.data })
);
break;
case "execution_success":
this.dispatchEvent(
new CustomEvent("execution_success", { detail: msg.data })
);
break;
case "execution_error":
this.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data })
@@ -315,10 +320,14 @@ class ComfyApi extends EventTarget {
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt) => ({
taskType: "Running",
prompt,
remove: { name: "Cancel", cb: () => api.interrupt() },
})),
Pending: data.queue_pending.map((prompt) => ({ prompt })),
Pending: data.queue_pending.map((prompt) => ({
taskType: "Pending",
prompt,
})),
};
} catch (error) {
console.error(error);
@@ -335,7 +344,11 @@ class ComfyApi extends EventTarget {
): Promise<{ History: HistoryTaskItem[] }> {
try {
const res = await this.fetchApi(`/history?max_items=${max_items}`);
return { History: Object.values(await res.json()) };
return {
History: Object.values(await res.json()).map(
(item: HistoryTaskItem) => ({ ...item, taskType: "History" })
),
};
} catch (error) {
console.error(error);
return { History: [] };

View File

@@ -1845,13 +1845,18 @@ export class ComfyApp {
await this.#setUser();
// Create and mount the LiteGraph in the DOM
const canvasContainer = document.createElement("div");
canvasContainer.id = "graph-canvas-container";
const mainCanvas = document.createElement("canvas");
mainCanvas.style.touchAction = "none";
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, {
id: "graph-canvas",
}));
canvasEl.tabIndex = 1;
document.body.prepend(canvasEl);
canvasContainer.prepend(canvasEl);
document.body.prepend(canvasContainer);
this.resizeCanvas();
await Promise.all([

View File

@@ -5,10 +5,8 @@ import { downloadBlob } from "../../utils";
import { ComfyButton } from "../components/button";
import { ComfyButtonGroup } from "../components/buttonGroup";
import { ComfySplitButton } from "../components/splitButton";
import { ComfyViewHistoryButton } from "./viewHistory";
import { ComfyQueueButton } from "./queueButton";
import { ComfyWorkflowsMenu } from "./workflows";
import { ComfyViewQueueButton } from "./viewQueue";
import { getInteruptButton } from "./interruptButton";
import "./menu.css";
import type { ComfySettingsDialog } from "../settings";
@@ -128,19 +126,10 @@ export class ComfyAppMenu {
},
})
);
this.settingsGroup = new ComfyButtonGroup(
new ComfyButton({
icon: "cog",
content: "Settings",
tooltip: "Open settings",
action: () => {
app.ui.settings.show();
},
})
);
// Keep the settings group as there are custom scripts attaching extra
// elements to it.
this.settingsGroup = new ComfyButtonGroup();
this.viewGroup = new ComfyButtonGroup(
new ComfyViewHistoryButton(app).element,
new ComfyViewQueueButton(app).element,
getInteruptButton("nlg-hide").element
);
this.mobileMenuButton = new ComfyButton({
@@ -193,6 +182,7 @@ export class ComfyAppMenu {
app.ui.menuContainer.style.removeProperty("display");
this.element.style.display = "none";
app.ui.restoreMenuPosition();
document.dispatchEvent(new Event("comfy:setting:beta-menu-disabled"));
}
window.dispatchEvent(new Event("resize"));
},

View File

@@ -1,26 +0,0 @@
import type { ComfyApp } from "@/scripts/app";
import { ComfyButton } from "../components/button";
import { ComfyViewList, ComfyViewListButton } from "./viewList";
export class ComfyViewHistoryButton extends ComfyViewListButton {
constructor(app: ComfyApp) {
super(app, {
button: new ComfyButton({
content: "View History",
icon: "history",
tooltip: "View history",
classList: "comfyui-button comfyui-history-button",
}),
list: ComfyViewHistoryList,
mode: "History",
});
}
}
export class ComfyViewHistoryList extends ComfyViewList {
async loadItems() {
const items = await super.loadItems();
items["History"].reverse();
return items;
}
}

View File

@@ -1,231 +0,0 @@
import { ComfyButton } from "../components/button";
import { $el } from "../../ui";
import { api } from "../../api";
import { ComfyPopup } from "../components/popup";
import type { ComfyApp } from "@/scripts/app";
type ViewListMode = "Queue" | "History";
export class ComfyViewListButton {
popup: ComfyPopup;
app: ComfyApp;
button: ComfyButton;
element: HTMLDivElement;
list: ComfyViewList;
get open() {
return this.popup.open;
}
set open(open) {
this.popup.open = open;
}
constructor(
app: ComfyApp,
{
button,
list,
mode,
}: { button: ComfyButton; list: typeof ComfyViewList; mode: ViewListMode }
) {
this.app = app;
this.button = button;
this.element = $el(
"div.comfyui-button-wrapper",
this.button.element
) as HTMLDivElement;
this.popup = new ComfyPopup({
target: this.element,
container: this.element,
horizontal: "right",
});
this.list = new (list ?? ComfyViewList)(app, mode, this.popup);
this.popup.children = [this.list.element];
this.popup.addEventListener("open", () => {
this.list.update();
});
this.popup.addEventListener("close", () => {
this.list.close();
});
this.button.withPopup(this.popup);
api.addEventListener("status", () => {
if (this.popup.open) {
this.popup.update();
}
});
}
}
export class ComfyViewList {
app: ComfyApp;
mode: ViewListMode;
popup: ComfyPopup;
type: string;
items: HTMLElement;
clear: ComfyButton;
refresh: ComfyButton;
element: HTMLElement;
constructor(app: ComfyApp, mode: ViewListMode, popup: ComfyPopup) {
this.app = app;
this.mode = mode;
this.popup = popup;
this.type = mode.toLowerCase();
this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`);
this.clear = new ComfyButton({
icon: "cancel",
content: "Clear",
action: async () => {
this.showSpinner(false);
await api.clearItems(this.type);
await this.update();
},
});
this.refresh = new ComfyButton({
icon: "refresh",
content: "Refresh",
action: async () => {
await this.update(false);
},
});
this.element = $el(
`div.comfyui-${this.type}-popup.comfyui-view-list-popup`,
[
$el("h3", mode),
$el("header", [this.clear.element, this.refresh.element]),
this.items,
]
);
api.addEventListener("status", () => {
if (this.popup.open) {
this.update();
}
});
}
async close() {
this.items.replaceChildren();
}
async update(resize = true) {
this.showSpinner(resize);
const res = await this.loadItems();
let any = false;
const names = Object.keys(res);
const sections = names
.map((section) => {
const items = res[section];
if (items?.length) {
any = true;
} else {
return;
}
const rows = [];
if (names.length > 1) {
rows.push($el("h5", section));
}
rows.push(...items.flatMap((item) => this.createRow(item, section)));
return $el("section", rows);
})
.filter(Boolean);
if (any) {
this.items.replaceChildren(...sections);
} else {
this.items.replaceChildren($el("h5", "None"));
}
this.popup.update();
this.clear.enabled = this.refresh.enabled = true;
this.element.style.removeProperty("height");
}
showSpinner(resize = true) {
// if (!this.spinner) {
// this.spinner = createSpinner();
// }
// if (!resize) {
// this.element.style.height = this.element.clientHeight + "px";
// }
// this.clear.enabled = this.refresh.enabled = false;
// this.items.replaceChildren(
// $el(
// "div",
// {
// style: {
// fontSize: "18px",
// },
// },
// this.spinner
// )
// );
// this.popup.update();
}
async loadItems() {
return await api.getItems(this.type);
}
getRow(item, section) {
return {
text: item.prompt[0] + "",
actions: [
{
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
} catch (error) {
alert("Error loading workflow: " + error.message);
console.error(error);
}
},
},
{
text: "Delete",
action: async () => {
try {
await api.deleteItem(this.type, item.prompt[1]);
this.update();
} catch (error) {}
},
},
],
};
}
createRow = (item, section) => {
const row = this.getRow(item, section);
return [
$el("span", row.text),
...row.actions.map(
(a) =>
new ComfyButton({
content: a.text,
action: async (e, btn) => {
btn.enabled = false;
try {
await a.action();
} catch (error) {
throw error;
} finally {
btn.enabled = true;
}
},
}).element
),
];
};
}

View File

@@ -1,56 +0,0 @@
import { ComfyButton } from "../components/button";
import { ComfyViewList, ComfyViewListButton } from "./viewList";
import { api } from "../../api";
import type { ComfyApp } from "@/scripts/app";
export class ComfyViewQueueButton extends ComfyViewListButton {
constructor(app: ComfyApp) {
super(app, {
button: new ComfyButton({
content: "View Queue",
icon: "format-list-numbered",
tooltip: "View queue",
classList: "comfyui-button comfyui-queue-button",
}),
list: ComfyViewQueueList,
mode: "Queue",
});
}
}
export class ComfyViewQueueList extends ComfyViewList {
getRow = (item, section) => {
if (section !== "Running") {
return super.getRow(item, section);
}
return {
text: item.prompt[0] + "",
actions: [
{
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
} catch (error) {
alert("Error loading workflow: " + error.message);
console.error(error);
}
},
},
{
text: "Cancel",
action: async () => {
try {
await api.interrupt();
} catch (error) {}
},
},
],
};
};
}