ComfyUI closes the side panel when a tab enters the App view; reopen the RigShare tab that was open, so it behaves the same in both directions.
150 lines
6.0 KiB
JavaScript
150 lines
6.0 KiB
JavaScript
// RigShare in ComfyUI's App mode.
|
|
//
|
|
// The App view's sidebar only draws buttons for a fixed set of tabs (assets
|
|
// and apps), but its docked side panel shows whichever sidebar tab is active.
|
|
// So RigShare adds its own buttons to that sidebar, copied from ComfyUI's so
|
|
// they look the same, and they open the regular RigShare and Files tabs in the
|
|
// docked panel, exactly like ComfyUI's own tabs. If the sidebar can't be
|
|
// found, a small floating dock offers the same buttons.
|
|
|
|
import { h } from "./ui.js";
|
|
|
|
const RAIL = 'nav[data-testid="side-toolbar"]';
|
|
|
|
export class AppDock {
|
|
/**
|
|
* views: { id: { icon, title, tabId } }, tabId being a registered sidebar tab.
|
|
*/
|
|
constructor({ app, sync, views }) {
|
|
this.app = app;
|
|
this.sync = sync;
|
|
this.views = views;
|
|
this.badges = {};
|
|
this.railButtons = {};
|
|
this.dockButtons = {};
|
|
this.dock = h("div", { class: "rs-dock rs-panel-vars" }, ...Object.entries(views).map(([id, v]) => {
|
|
const badge = h("span", { class: "rs-dock-badge" });
|
|
const btn = h("button", {
|
|
class: "rs-dock-btn", title: v.title, "aria-label": v.title,
|
|
onclick: () => this.toggle(id),
|
|
}, h("i", { class: `pi ${v.icon}` }), badge);
|
|
this.dockButtons[id] = { btn, badge };
|
|
return btn;
|
|
}));
|
|
this.dock.style.display = "none";
|
|
document.body.append(this.dock);
|
|
this.wasApp = false;
|
|
this.graphTab = null; // our tab open in the graph view, if any
|
|
this.restore = null; // { tabId, until }: reopen it after entering the App view
|
|
setInterval(() => this.update(), 300);
|
|
}
|
|
|
|
get activeTabId() {
|
|
return this.app.extensionManager?.sidebarTab?.activeSidebarTabId ?? null;
|
|
}
|
|
|
|
/** Open (or close) the tab in ComfyUI's docked side panel, like its own buttons do. */
|
|
toggle(id) {
|
|
this.app.extensionManager?.command?.execute(`Workspace.ToggleSidebarTab.${this.views[id].tabId}`);
|
|
setTimeout(() => this.refreshButtons(), 30);
|
|
}
|
|
|
|
update() {
|
|
const active = this.sync.inAppMode;
|
|
this.keepOpenIntoApp(active);
|
|
const rail = active ? document.querySelector(RAIL) : null;
|
|
if (rail) this.addToRail(rail);
|
|
else this.removeFromRail();
|
|
this.dock.style.display = active && !rail ? "" : "none";
|
|
this.refreshButtons();
|
|
}
|
|
|
|
/**
|
|
* ComfyUI closes the side panel when a tab enters the App view (not when it
|
|
* goes back to the graph). If a RigShare tab was open, open it again.
|
|
*/
|
|
keepOpenIntoApp(inApp) {
|
|
const open = this.activeTabId;
|
|
const ours = Object.values(this.views).map((v) => v.tabId);
|
|
if (!inApp) {
|
|
this.graphTab = ours.includes(open) ? open : null;
|
|
this.restore = null;
|
|
} else if (!this.wasApp && this.graphTab) {
|
|
this.restore = { tabId: this.graphTab, until: Date.now() + 1500 };
|
|
}
|
|
this.wasApp = inApp;
|
|
if (!this.restore) return;
|
|
if (open === this.restore.tabId || Date.now() > this.restore.until) {
|
|
this.restore = null;
|
|
} else if (open === null) {
|
|
this.app.extensionManager?.command?.execute(`Workspace.ToggleSidebarTab.${this.restore.tabId}`);
|
|
}
|
|
}
|
|
|
|
/** Copy one of ComfyUI's sidebar buttons for each view, placed after its own tabs. */
|
|
addToRail(rail) {
|
|
const ours = Object.values(this.railButtons);
|
|
if (ours.length && ours.every((b) => rail.contains(b.btn))) return;
|
|
const tabs = [...rail.querySelectorAll('[data-testid$="-tab-button"]')].filter((b) => !b.dataset.rigshare);
|
|
const template = tabs.at(-1);
|
|
if (!template) return;
|
|
this.removeFromRail();
|
|
let after = template;
|
|
for (const [id, view] of Object.entries(this.views)) {
|
|
const btn = template.cloneNode(true);
|
|
btn.removeAttribute("data-testid");
|
|
btn.dataset.rigshare = id;
|
|
btn.classList.remove("side-bar-button-selected");
|
|
btn.setAttribute("aria-label", view.title);
|
|
btn.title = view.title;
|
|
const icon = btn.querySelector(".side-bar-button-icon");
|
|
if (icon) {
|
|
const mine = document.createElement("i");
|
|
mine.className = `pi ${view.icon} side-bar-button-icon`;
|
|
mine.style.fontSize = "18px";
|
|
icon.replaceWith(mine);
|
|
}
|
|
btn.querySelector(".sidebar-icon-badge")?.remove();
|
|
const label = [...btn.querySelectorAll("span")].find((s) => s.textContent.trim());
|
|
if (label) label.textContent = view.title;
|
|
const badge = document.createElement("span");
|
|
badge.className = "sidebar-icon-badge rs-rail-badge";
|
|
const holder = btn.querySelector(".side-bar-button-icon")?.parentElement ?? btn;
|
|
holder.style.position = "relative"; // the badge sits on the icon's corner
|
|
holder.append(badge);
|
|
btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); this.toggle(id); });
|
|
after.after(btn);
|
|
after = btn;
|
|
this.railButtons[id] = { btn, badge };
|
|
}
|
|
this.refreshButtons();
|
|
}
|
|
|
|
removeFromRail() {
|
|
for (const b of Object.values(this.railButtons)) b.btn.remove();
|
|
this.railButtons = {};
|
|
}
|
|
|
|
refreshButtons() {
|
|
const open = this.activeTabId;
|
|
for (const [id, b] of Object.entries(this.railButtons)) {
|
|
b.btn.classList.toggle("side-bar-button-selected", this.views[id].tabId === open);
|
|
this.paintBadge(b.badge, this.badges[id]);
|
|
}
|
|
for (const [id, b] of Object.entries(this.dockButtons)) {
|
|
b.btn.classList.toggle("active", this.views[id].tabId === open);
|
|
this.paintBadge(b.badge, this.badges[id]);
|
|
}
|
|
}
|
|
|
|
paintBadge(el, text) {
|
|
el.textContent = text || "";
|
|
el.style.display = text ? "block" : "none";
|
|
}
|
|
|
|
setBadge(id, text) {
|
|
this.badges[id] = text || null;
|
|
this.refreshButtons();
|
|
}
|
|
}
|