Files

289 lines
9.2 KiB
JavaScript

import { app } from "/scripts/app.js";
import { ComfyButtonGroup } from "/scripts/ui/components/buttonGroup.js";
import { ComfyButton } from "/scripts/ui/components/button.js";
const BUTTON_GROUP_CLASS = "gemini-image-browser-top-menu-group";
const BUTTON_TOOLTIP = "Open Gemini Image Browser (Ctrl+B)";
const IMAGE_BROWSER_PATH = "/gemini-image-browser/ui";
const STORAGE_KEY_MODE = "geminiImageBrowserOpenMode";
const STORAGE_KEY_RECT = "geminiMiniWindowRect";
let miniWindowContainer = null;
function getOpenMode() {
return localStorage.getItem(STORAGE_KEY_MODE) || "tab";
}
function getSavedRect() {
try {
const raw = localStorage.getItem(STORAGE_KEY_RECT);
if (raw) return JSON.parse(raw);
} catch (_) { /* ignore */ }
return null;
}
function saveRect(rect) {
localStorage.setItem(STORAGE_KEY_RECT, JSON.stringify(rect));
}
function closeMiniWindow() {
if (miniWindowContainer) {
// Save position and size before removing
const frame = miniWindowContainer.querySelector(".gemini-mini-frame");
if (frame) {
saveRect({
left: frame.offsetLeft,
top: frame.offsetTop,
width: frame.offsetWidth,
height: frame.offsetHeight,
});
}
miniWindowContainer.remove();
miniWindowContainer = null;
}
}
function getCurrentRect() {
if (!miniWindowContainer) return null;
const frame = miniWindowContainer.querySelector(".gemini-mini-frame");
if (!frame) return null;
return {
left: frame.offsetLeft,
top: frame.offsetTop,
width: frame.offsetWidth,
height: frame.offsetHeight,
};
}
// Save rect on page close/refresh so resizes are never lost
window.addEventListener("beforeunload", () => {
const rect = getCurrentRect();
if (rect) saveRect(rect);
});
function openMiniWindow() {
if (miniWindowContainer) {
// Already open — bring to front
miniWindowContainer.style.zIndex = "9999";
return;
}
const saved = getSavedRect();
const defaultW = 520;
const defaultH = 620;
const left = saved?.left ?? Math.round((window.innerWidth - defaultW) / 2);
const top = saved?.top ?? Math.round((window.innerHeight - defaultH) / 2);
const width = saved?.width ?? defaultW;
const height = saved?.height ?? defaultH;
// Overlay container (captures nothing, just positions the window)
miniWindowContainer = document.createElement("div");
miniWindowContainer.className = "gemini-mini-overlay";
miniWindowContainer.style.cssText = `
position: fixed; inset: 0; z-index: 9999;
pointer-events: none;
`;
// The floating frame
const frame = document.createElement("div");
frame.className = "gemini-mini-frame";
frame.style.cssText = `
position: absolute;
left: ${left}px; top: ${top}px;
width: ${width}px; height: ${height}px;
min-width: 520px; min-height: 520px;
display: flex; flex-direction: column;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05);
overflow: hidden;
pointer-events: auto;
resize: both;
`;
// Title bar
const titleBar = document.createElement("div");
titleBar.className = "gemini-mini-titlebar";
titleBar.style.cssText = `
display: flex; align-items: center; justify-content: space-between;
padding: 0 12px; height: 38px; min-height: 38px;
background: linear-gradient(180deg, #2e2e2e 0%, #252525 100%);
border-bottom: 1px solid #3a3a3a;
cursor: grab; user-select: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px; color: #aaa; letter-spacing: 0.03em;
`;
const titleText = document.createElement("span");
titleText.textContent = "Image Browser";
titleText.style.cssText = "font-weight: 600; color: #ccc;";
const closeBtn = document.createElement("button");
closeBtn.textContent = "✕";
closeBtn.style.cssText = `
background: none; border: none; color: #888; cursor: pointer;
font-size: 14px; width: 26px; height: 26px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
`;
closeBtn.addEventListener("mouseenter", () => { closeBtn.style.background = "#dc3545"; closeBtn.style.color = "#fff"; });
closeBtn.addEventListener("mouseleave", () => { closeBtn.style.background = "none"; closeBtn.style.color = "#888"; });
closeBtn.addEventListener("click", closeMiniWindow);
titleBar.appendChild(titleText);
titleBar.appendChild(closeBtn);
// Iframe
const iframe = document.createElement("iframe");
iframe.src = IMAGE_BROWSER_PATH + "?mode=mini";
iframe.style.cssText = `
flex: 1; width: 100%; border: none;
background: #1a1a1a;
`;
frame.appendChild(titleBar);
frame.appendChild(iframe);
miniWindowContainer.appendChild(frame);
document.body.appendChild(miniWindowContainer);
// --- Dragging ---
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let frameStartX = 0, frameStartY = 0;
titleBar.addEventListener("mousedown", (e) => {
if (e.target === closeBtn) return;
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
frameStartX = frame.offsetLeft;
frameStartY = frame.offsetTop;
titleBar.style.cursor = "grabbing";
// Prevent iframe from capturing mouse events during drag
iframe.style.pointerEvents = "none";
e.preventDefault();
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
frame.style.left = (frameStartX + dx) + "px";
frame.style.top = (frameStartY + dy) + "px";
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
titleBar.style.cursor = "grab";
iframe.style.pointerEvents = "";
// Save position
saveRect({
left: frame.offsetLeft,
top: frame.offsetTop,
width: frame.offsetWidth,
height: frame.offsetHeight,
});
}
});
// Save size on resize (debounced for reliability)
let resizeTimer = null;
const resizeObserver = new ResizeObserver(() => {
if (!isDragging) {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
saveRect({
left: frame.offsetLeft,
top: frame.offsetTop,
width: frame.offsetWidth,
height: frame.offsetHeight,
});
}, 200);
}
});
resizeObserver.observe(frame);
}
// Listen for messages from the iframe
window.addEventListener("message", (e) => {
if (e.data?.type === "gemini-browser-switch-to-tab") {
closeMiniWindow();
localStorage.setItem(STORAGE_KEY_MODE, "tab");
window.open(IMAGE_BROWSER_PATH, "_blank");
}
});
const openImageBrowser = () => {
const mode = getOpenMode();
if (mode === "mini") {
if (miniWindowContainer) {
closeMiniWindow();
} else {
openMiniWindow();
}
} else {
window.open(IMAGE_BROWSER_PATH, "_blank");
}
};
const createTopMenuButton = () => {
const button = new ComfyButton({
icon: "gemini-imagebrowser",
tooltip: BUTTON_TOOLTIP,
app,
enabled: true,
classList: "comfyui-button comfyui-menu-mobile-collapse primary",
});
button.element.setAttribute("aria-label", BUTTON_TOOLTIP);
button.element.title = BUTTON_TOOLTIP;
if (button.iconElement) {
button.iconElement.textContent = "🖼️";
button.iconElement.style.fontSize = "1.2rem";
}
button.element.addEventListener("click", openImageBrowser);
return button;
};
const attachTopMenuButton = (attempt = 0) => {
if (document.querySelector(`.${BUTTON_GROUP_CLASS}`)) return;
const MAX_ATTACH_ATTEMPTS = 120;
const settingsGroup = app.menu?.settingsGroup;
if (!settingsGroup?.element?.parentElement) {
if (attempt >= MAX_ATTACH_ATTEMPTS) {
console.warn("Gemini Image Browser: settingsGroup not found");
return;
}
requestAnimationFrame(() => attachTopMenuButton(attempt + 1));
return;
}
const button = createTopMenuButton();
const group = new ComfyButtonGroup(button);
group.element.classList.add(BUTTON_GROUP_CLASS);
settingsGroup.element.before(group.element);
console.log("Gemini Image Browser: Button added to menu.");
};
app.registerExtension({
name: "Gemini.ImageBrowser.Button",
setup() {
console.log("Gemini Image Browser: Setting up menu button.");
attachTopMenuButton();
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.key === "b") {
e.preventDefault();
openImageBrowser();
}
});
},
});