- Implement full web interface with Go html/template server - Add GX component library (buttons, dialogs, tables, forms, etc.) - Create scene/performer/studio/movie detail and listing pages - Add Adult Empire scraper for additional metadata sources - Implement movie support with database schema - Add import and sync services for data management - Include comprehensive API and frontend documentation - Add custom color scheme and responsive layout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
116 lines
3.2 KiB
JavaScript
116 lines
3.2 KiB
JavaScript
<script>
|
||
const gxMenu = document.getElementById("gx-contextmenu");
|
||
let gxMenuItems = [];
|
||
let gxMenuIndex = -1;
|
||
|
||
/* ------ OPEN MENU ------ */
|
||
function openContextMenu(x, y, items) {
|
||
// Build HTML
|
||
gxMenu.innerHTML = items.map(item => {
|
||
if (item === "divider") {
|
||
return `<li class="gx-contextmenu-divider"></li>`;
|
||
}
|
||
|
||
return `
|
||
<li class="gx-contextmenu-item" role="menuitem" data-action="${item.action}">
|
||
${item.label}
|
||
${item.submenu ? `<span class="submenu-arrow">›</span>` : ""}
|
||
</li>
|
||
`;
|
||
}).join("");
|
||
|
||
gxMenuItems = Array.from(gxMenu.querySelectorAll(".gx-contextmenu-item"));
|
||
gxMenuIndex = -1;
|
||
|
||
// Position
|
||
gxMenu.style.left = x + "px";
|
||
gxMenu.style.top = y + "px";
|
||
|
||
// Prevent offscreen overflow
|
||
const rect = gxMenu.getBoundingClientRect();
|
||
if (rect.right > window.innerWidth) {
|
||
gxMenu.style.left = (x - rect.width) + "px";
|
||
}
|
||
if (rect.bottom > window.innerHeight) {
|
||
gxMenu.style.top = (y - rect.height) + "px";
|
||
}
|
||
|
||
// Show
|
||
gxMenu.classList.add("show");
|
||
}
|
||
|
||
/* ------ CLOSE ------ */
|
||
function closeContextMenu() {
|
||
gxMenu.classList.remove("show");
|
||
gxMenuIndex = -1;
|
||
}
|
||
|
||
/* ------ CLICK HANDLER ------ */
|
||
gxMenu.addEventListener("click", e => {
|
||
const li = e.target.closest(".gx-contextmenu-item");
|
||
if (!li) return;
|
||
|
||
const action = li.dataset.action;
|
||
if (action && window[action]) {
|
||
window[action]();
|
||
}
|
||
|
||
closeContextMenu();
|
||
});
|
||
|
||
/* ------ GLOBAL RIGHT CLICK ------ */
|
||
document.addEventListener("contextmenu", e => {
|
||
e.preventDefault();
|
||
|
||
const x = e.clientX;
|
||
const y = e.clientY;
|
||
|
||
// Example menu set (replace per-page as needed)
|
||
const menuItems = window.getDynamicContextMenu
|
||
? window.getDynamicContextMenu(e)
|
||
: [
|
||
{ label: "Import Performer", action: "importPerformer" },
|
||
{ label: "Open Scene", action: "openScene" },
|
||
"divider",
|
||
{ label: "Copy ID", action: "copyID" },
|
||
{ label: "Delete", action: "deleteEntity" }
|
||
];
|
||
|
||
openContextMenu(x, y, menuItems);
|
||
});
|
||
|
||
/* ------ CLICK OUTSIDE ------ */
|
||
document.addEventListener("click", e => {
|
||
if (!gxMenu.contains(e.target)) closeContextMenu();
|
||
});
|
||
|
||
/* ------ ESC ------ */
|
||
document.addEventListener("keydown", e => {
|
||
if (e.key === "Escape") closeContextMenu();
|
||
});
|
||
|
||
/* ------ KEYBOARD NAV ------ */
|
||
document.addEventListener("keydown", e => {
|
||
if (!gxMenu.classList.contains("show")) return;
|
||
|
||
if (e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
gxMenuIndex = (gxMenuIndex + 1) % gxMenuItems.length;
|
||
} else if (e.key === "ArrowUp") {
|
||
e.preventDefault();
|
||
gxMenuIndex = (gxMenuIndex - 1 + gxMenuItems.length) % gxMenuItems.length;
|
||
} else if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
if (gxMenuIndex >= 0) {
|
||
const li = gxMenuItems[gxMenuIndex];
|
||
const action = li.dataset.action;
|
||
if (action && window[action]) window[action]();
|
||
}
|
||
closeContextMenu();
|
||
}
|
||
|
||
gxMenuItems.forEach(i => i.classList.remove("focused"));
|
||
if (gxMenuIndex >= 0) gxMenuItems[gxMenuIndex].classList.add("focused");
|
||
});
|
||
</script>
|