378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
import { EVENTS } from "../../app/events.js";
|
|
import { t } from "../../app/i18n.js";
|
|
import { setTooltipHint } from "../../utils/tooltipShortcuts.js";
|
|
|
|
const BUTTON_HOST_ATTR = "data-mjr-topbar-mfv-host";
|
|
const BUTTON_ATTR = "data-mjr-topbar-mfv-button";
|
|
const SLOT_ATTR = "data-mjr-topbar-mfv-slot";
|
|
const BUTTON_LABEL_KEY = "label.floatingViewer";
|
|
const BUTTON_LABEL_FALLBACK = "Viewer";
|
|
const MFV_TOOLTIP_HINT = "V";
|
|
const ACTIONBAR_SELECTORS = [
|
|
".actionbar-container",
|
|
"[data-testid='actionbar-container']",
|
|
"[data-testid='topbar']",
|
|
".comfyui-topbar",
|
|
".topbar",
|
|
];
|
|
const BUTTON_GROUP_SELECTORS = [
|
|
".queue-button-group",
|
|
".comfyui-button-group",
|
|
"[role='toolbar']",
|
|
];
|
|
const MANAGER_BUTTON_SELECTORS = [
|
|
"[data-testid*='manager' i]",
|
|
"[aria-label*='manager' i]",
|
|
"[title*='manager' i]",
|
|
"[data-command-id*='manager' i]",
|
|
"[data-command-id*='mjr.openAssetsManager' i]",
|
|
"button",
|
|
];
|
|
|
|
let _observer: any = null;
|
|
let _observedTarget: any = null;
|
|
let _bodyObserver: any = null;
|
|
let _visibilityListener: any = null;
|
|
let _resizeListener: any = null;
|
|
let _syncQueued = false;
|
|
let _syncTimer: any = null;
|
|
let _visible = false;
|
|
let _hasVisibilitySignal = false;
|
|
|
|
function _tryObserveActionbar(container: any) {
|
|
if (_observer && _observedTarget === container) return;
|
|
if (!container) {
|
|
// Actionbar not in DOM yet — observe body shallowly to detect it
|
|
if (!_bodyObserver && typeof MutationObserver !== "undefined") {
|
|
_bodyObserver = new MutationObserver(() => {
|
|
const c = getActionbarContainer();
|
|
if (c) {
|
|
_bodyObserver?.disconnect?.();
|
|
_bodyObserver = null;
|
|
scheduleSync();
|
|
}
|
|
});
|
|
_bodyObserver.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
return;
|
|
}
|
|
// Disconnect previous observer if target changed
|
|
try {
|
|
_observer?.disconnect?.();
|
|
} catch (_) {
|
|
/* noop */
|
|
}
|
|
_observer = new MutationObserver(() => scheduleSync());
|
|
_observer.observe(container, { childList: true, subtree: true });
|
|
_observedTarget = container;
|
|
// Body observer no longer needed
|
|
try {
|
|
_bodyObserver?.disconnect?.();
|
|
} catch (_) {
|
|
/* noop */
|
|
}
|
|
_bodyObserver = null;
|
|
}
|
|
|
|
function getActionbarContainer() {
|
|
if (typeof document === "undefined") return null;
|
|
for (const selector of ACTIONBAR_SELECTORS) {
|
|
const el = document.querySelector(selector);
|
|
if (el) return el;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function updateViewerTopOffset(container = getActionbarContainer()) {
|
|
if (typeof document === "undefined") return;
|
|
const rootStyle = document.documentElement?.style;
|
|
if (!rootStyle) return;
|
|
|
|
if (!container) {
|
|
rootStyle.setProperty("--mjr-mfv-top-offset", "60px");
|
|
return;
|
|
}
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const safeTop = Math.max(60, Math.ceil(rect.bottom + 12));
|
|
rootStyle.setProperty("--mjr-mfv-top-offset", `${safeTop}px`);
|
|
}
|
|
|
|
function getAnchor(container: any) {
|
|
if (!container) return null;
|
|
const managerButton = getManagerButtonAnchor(container);
|
|
if (managerButton) return managerButton;
|
|
for (const selector of BUTTON_GROUP_SELECTORS) {
|
|
const el = container.querySelector(selector);
|
|
if (el) return el;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isManagerLikeButton(el: any) {
|
|
try {
|
|
const text = String(el?.textContent || "").trim().toLowerCase();
|
|
const label = String(
|
|
el?.getAttribute?.("aria-label") ||
|
|
el?.getAttribute?.("title") ||
|
|
el?.getAttribute?.("data-testid") ||
|
|
el?.getAttribute?.("data-command-id") ||
|
|
"",
|
|
).toLowerCase();
|
|
return (
|
|
text === "manager" ||
|
|
text.includes("assets manager") ||
|
|
text.includes("majoor assets manager") ||
|
|
label.includes("manager") ||
|
|
label.includes("mjr.openassetsmanager")
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getManagerButtonAnchor(container: any) {
|
|
if (!container?.querySelectorAll) return null;
|
|
for (const selector of MANAGER_BUTTON_SELECTORS) {
|
|
try {
|
|
const candidates = Array.from(container.querySelectorAll(selector));
|
|
const found = candidates.find((el: any) => isManagerLikeButton(el));
|
|
if (found) return found;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getTopbarButtonParent(container: any, anchor: any) {
|
|
if (!container) return null;
|
|
if (anchor?.classList?.contains?.("comfyui-button-group")) return anchor;
|
|
return anchor?.parentElement || container;
|
|
}
|
|
|
|
function ensureSlotMounted(container: any) {
|
|
if (!container) return null;
|
|
|
|
let slot = container.querySelector(`[${SLOT_ATTR}]`);
|
|
if (!slot) {
|
|
slot = document.createElement("div");
|
|
slot.setAttribute(SLOT_ATTR, "1");
|
|
slot.className = "flex h-full items-center pointer-events-auto";
|
|
slot.style.position = "relative";
|
|
slot.style.zIndex = "10030";
|
|
slot.style.padding = "0 4px";
|
|
}
|
|
|
|
const anchor = getAnchor(container);
|
|
const parent = getTopbarButtonParent(container, anchor);
|
|
|
|
if (slot.parentElement !== parent) {
|
|
if (anchor && parent && parent !== anchor) {
|
|
parent.insertBefore(slot, anchor.nextSibling);
|
|
} else if (parent) {
|
|
parent.appendChild(slot);
|
|
}
|
|
} else if (anchor && parent && parent !== anchor && slot.previousSibling !== anchor) {
|
|
parent.insertBefore(slot, anchor.nextSibling);
|
|
} else if (!anchor && slot !== parent.lastElementChild) {
|
|
parent.appendChild(slot);
|
|
}
|
|
|
|
return slot;
|
|
}
|
|
|
|
function createIcon() {
|
|
const icon = document.createElement("i");
|
|
icon.className = "mjr-topbar-mfv-icon pi pi-eye";
|
|
icon.setAttribute("aria-hidden", "true");
|
|
return icon;
|
|
}
|
|
|
|
function createLabel(text = "Viewer") {
|
|
const label = document.createElement("span");
|
|
label.className = "mjr-topbar-mfv-label";
|
|
label.textContent = text;
|
|
return label;
|
|
}
|
|
|
|
function updateButtonState(button: any) {
|
|
if (!button) return;
|
|
const tooltipLabel = _visible
|
|
? t("tooltip.closeMFV", "Close Majoor Floating Viewer")
|
|
: t("tooltip.openMFV", "Open Majoor Floating Viewer");
|
|
setTooltipHint(button, tooltipLabel, MFV_TOOLTIP_HINT, {
|
|
ariaLabel: tooltipLabel,
|
|
});
|
|
button.setAttribute("aria-pressed", _visible ? "true" : "false");
|
|
button.classList.toggle("primary", _visible);
|
|
button.classList.toggle("mjr-topbar-mfv-active", _visible);
|
|
button.replaceChildren(
|
|
createIcon(),
|
|
createLabel(t(BUTTON_LABEL_KEY, BUTTON_LABEL_FALLBACK)),
|
|
);
|
|
}
|
|
|
|
function dispatchLaunch() {
|
|
try {
|
|
window.dispatchEvent(new Event(EVENTS.MFV_TOGGLE));
|
|
} catch (e) {
|
|
console.debug?.("[Majoor] top bar MFV launch failed", e);
|
|
}
|
|
scheduleSync();
|
|
}
|
|
|
|
function createButton() {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.setAttribute(BUTTON_ATTR, "1");
|
|
button.setAttribute("data-command-id", "mjr.toggleFloatingViewer");
|
|
button.className = "comfyui-button p-button p-component mjr-topbar-mfv-button";
|
|
button.style.position = "relative";
|
|
button.style.zIndex = "10030";
|
|
button.style.width = "auto";
|
|
button.style.height = "32px";
|
|
button.style.minWidth = "32px";
|
|
button.style.padding = "0 8px";
|
|
button.style.gap = "8px";
|
|
button.style.display = "inline-flex";
|
|
button.style.alignItems = "center";
|
|
button.style.justifyContent = "center";
|
|
button.style.whiteSpace = "nowrap";
|
|
button.addEventListener("click", dispatchLaunch);
|
|
updateButtonState(button);
|
|
return button;
|
|
}
|
|
|
|
function createButtonHost() {
|
|
const host = document.createElement("div");
|
|
host.setAttribute(BUTTON_HOST_ATTR, "1");
|
|
host.className = "mjr-topbar-mfv-button-host";
|
|
host.style.position = "relative";
|
|
|
|
host.appendChild(createButton());
|
|
return host;
|
|
}
|
|
|
|
function _syncVisibleFromDom() {
|
|
if (typeof document === "undefined") return;
|
|
if (_hasVisibilitySignal) return;
|
|
const viewerOpen = !!document.querySelector(".mjr-mfv.is-visible");
|
|
if (_visible !== viewerOpen) {
|
|
_visible = viewerOpen;
|
|
}
|
|
}
|
|
|
|
function ensureButtonMounted() {
|
|
const container = getActionbarContainer();
|
|
if (!container) {
|
|
updateViewerTopOffset(null);
|
|
return null;
|
|
}
|
|
|
|
_tryObserveActionbar(container);
|
|
_syncVisibleFromDom();
|
|
|
|
const slot = ensureSlotMounted(container);
|
|
if (!slot) {
|
|
updateViewerTopOffset(container);
|
|
return null;
|
|
}
|
|
|
|
let host = slot.querySelector(`[${BUTTON_HOST_ATTR}]`);
|
|
if (!host) {
|
|
host = createButtonHost();
|
|
}
|
|
|
|
if (host.parentElement !== slot) {
|
|
slot.replaceChildren(host);
|
|
}
|
|
|
|
updateViewerTopOffset(container);
|
|
updateButtonState(host.querySelector(`[${BUTTON_ATTR}]`));
|
|
return slot;
|
|
}
|
|
|
|
function scheduleSync() {
|
|
if (_syncQueued) return;
|
|
_syncQueued = true;
|
|
_syncTimer = window.setTimeout(() => {
|
|
_syncQueued = false;
|
|
_syncTimer = null;
|
|
ensureButtonMounted();
|
|
}, 32);
|
|
}
|
|
|
|
export function mountTopBarMfvButton(): boolean {
|
|
if (typeof window === "undefined" || typeof document === "undefined" || !document.body) {
|
|
return false;
|
|
}
|
|
|
|
scheduleSync();
|
|
|
|
if (!_visibilityListener) {
|
|
_visibilityListener = (event: any) => {
|
|
_visible = Boolean(event?.detail?.visible);
|
|
_hasVisibilitySignal = true;
|
|
scheduleSync();
|
|
};
|
|
window.addEventListener(EVENTS.MFV_VISIBILITY_CHANGED, _visibilityListener);
|
|
}
|
|
|
|
if (!_resizeListener) {
|
|
_resizeListener = () => scheduleSync();
|
|
window.addEventListener("resize", _resizeListener);
|
|
}
|
|
|
|
_tryObserveActionbar(getActionbarContainer());
|
|
|
|
return true;
|
|
}
|
|
|
|
export function teardownTopBarMfvButton(): void {
|
|
try {
|
|
_observer?.disconnect?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
_observer = null;
|
|
_observedTarget = null;
|
|
|
|
try {
|
|
_bodyObserver?.disconnect?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
_bodyObserver = null;
|
|
|
|
try {
|
|
if (_syncTimer) {
|
|
window.clearTimeout(_syncTimer);
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
_syncTimer = null;
|
|
|
|
if (_visibilityListener && typeof window !== "undefined") {
|
|
window.removeEventListener(EVENTS.MFV_VISIBILITY_CHANGED, _visibilityListener);
|
|
}
|
|
_visibilityListener = null;
|
|
|
|
if (_resizeListener && typeof window !== "undefined") {
|
|
window.removeEventListener("resize", _resizeListener);
|
|
}
|
|
_resizeListener = null;
|
|
_syncQueued = false;
|
|
_visible = false;
|
|
_hasVisibilitySignal = false;
|
|
|
|
try {
|
|
document.documentElement?.style?.setProperty("--mjr-mfv-top-offset", "60px");
|
|
document.querySelector(`[${BUTTON_HOST_ATTR}]`)?.remove?.();
|
|
document.querySelector(`[${SLOT_ATTR}]`)?.remove?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|