289 lines
9.2 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
},
|
|
});
|