Files
Edouard Durand 7e10db82df App mode: keep an open RigShare or Files panel when entering the App view
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.
2026-09-26 07:17:49 +00:00

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();
}
}