(function () { const sessionId = window.location.pathname.split("/").pop(); const canvas = document.getElementById("screen"); const ctx = canvas.getContext("2d"); const addressBar = document.getElementById("address-bar"); const statusEl = document.getElementById("status"); const overlay = document.getElementById("overlay"); const overlayMsg = document.getElementById("overlay-msg"); let ws = null; let viewportWidth = 1280; let viewportHeight = 720; let scaleX = 1; let scaleY = 1; let pingTimer = null; const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${wsProtocol}//${window.location.host}/ws/${sessionId}`; function setStatus(text) { statusEl.textContent = text; } function showOverlay(message) { overlayMsg.textContent = message; overlay.classList.remove("hidden"); } function mapCoords(clientX, clientY) { const rect = canvas.getBoundingClientRect(); const x = (clientX - rect.left) / scaleX; const y = (clientY - rect.top) / scaleY; return { x, y }; } function send(payload) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload)); } } function drawFrame(blob) { const img = new Image(); const url = URL.createObjectURL(blob); img.onload = () => { if (canvas.width !== img.width || canvas.height !== img.height) { canvas.width = img.width; canvas.height = img.height; viewportWidth = img.width; viewportHeight = img.height; updateScale(); } ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); }; img.src = url; } function updateScale() { const rect = canvas.getBoundingClientRect(); scaleX = rect.width / viewportWidth; scaleY = rect.height / viewportHeight; } function connect() { ws = new WebSocket(wsUrl); ws.binaryType = "arraybuffer"; ws.onopen = () => { setStatus("已连接"); pingTimer = setInterval(() => send({ type: "ping" }), 60000); }; ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { drawFrame(new Blob([event.data], { type: "image/jpeg" })); return; } try { const msg = JSON.parse(event.data); if (msg.type === "init") { viewportWidth = msg.width; viewportHeight = msg.height; addressBar.value = msg.url || ""; updateScale(); } else if (msg.type === "url" || msg.type === "url_update") { addressBar.value = msg.url || ""; } else if (msg.type === "closed") { showOverlay("会话已结束"); ws.close(); } else if (msg.type === "error") { setStatus(msg.message); } } catch (_) { /* ignore */ } }; ws.onclose = () => { setStatus("已断开"); clearInterval(pingTimer); }; ws.onerror = () => { setStatus("连接错误"); }; } canvas.addEventListener("click", (e) => { canvas.focus(); const { x, y } = mapCoords(e.clientX, e.clientY); send({ action: "click", x, y, button: "left" }); }); canvas.addEventListener("dblclick", (e) => { e.preventDefault(); const { x, y } = mapCoords(e.clientX, e.clientY); send({ action: "dblclick", x, y }); }); canvas.addEventListener("wheel", (e) => { e.preventDefault(); send({ action: "wheel", deltaX: e.deltaX, deltaY: e.deltaY }); }, { passive: false }); canvas.addEventListener("mousemove", (e) => { const { x, y } = mapCoords(e.clientX, e.clientY); send({ action: "mousemove", x, y }); }); const specialKeys = new Set([ "Enter", "Backspace", "Delete", "Tab", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", ]); canvas.addEventListener("keydown", (e) => { e.preventDefault(); if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { send({ action: "type", text: e.key }); } else if (specialKeys.has(e.key)) { send({ action: "press", key: e.key }); } else { send({ action: "keydown", key: e.key }); } }); canvas.addEventListener("keyup", (e) => { e.preventDefault(); if (e.key.length > 1 || e.ctrlKey || e.metaKey || e.altKey) { send({ action: "keyup", key: e.key }); } }); window.addEventListener("resize", updateScale); async function apiPost(path) { const res = await fetch(path, { method: "POST", credentials: "include" }); if (res.status === 401) { window.location.href = "/"; return null; } return res.json(); } async function ensureAuth() { const res = await fetch("/api/auth/me", { credentials: "include" }); if (!res.ok) { window.location.href = "/"; return false; } return true; } document.getElementById("btn-back").addEventListener("click", async () => { const data = await apiPost(`/api/session/${sessionId}/back`); if (data) addressBar.value = data.url; }); document.getElementById("btn-forward").addEventListener("click", async () => { const data = await apiPost(`/api/session/${sessionId}/forward`); if (data) addressBar.value = data.url; }); document.getElementById("btn-reload").addEventListener("click", async () => { const data = await apiPost(`/api/session/${sessionId}/reload`); if (data) addressBar.value = data.url; }); function navigateTo(url) { send({ type: "navigate", url }); } document.getElementById("btn-go").addEventListener("click", () => { navigateTo(addressBar.value.trim()); }); addressBar.addEventListener("keydown", (e) => { if (e.key === "Enter") { navigateTo(addressBar.value.trim()); } }); document.getElementById("btn-close").addEventListener("click", async () => { await fetch(`/api/session/${sessionId}`, { method: "DELETE", credentials: "include" }); showOverlay("会话已关闭"); if (ws) ws.close(); }); ensureAuth().then((ok) => { if (ok) { connect(); canvas.focus(); } }); })();