110 lines
6.4 KiB
HTML
110 lines
6.4 KiB
HTML
<!doctype html>
|
||
<html lang="zh-Hant">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||
<meta name="theme-color" content="#16181d">
|
||
<title>Bot 的電腦</title>
|
||
<link rel="icon" href="icon.svg">
|
||
<style>
|
||
:root { --bg: #16181d; --bar: #1f2228; --line: #2e323a; --text: #eceef2; --dim: #9aa1ad; --accent: #4f8cff; --warn: #e5a13a; --ok: #3fb67a; }
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 15px/1.4 -apple-system, BlinkMacSystemFont, "PingFang TC", "Noto Sans TC", sans-serif; overflow: hidden; }
|
||
body { display: flex; flex-direction: column; height: 100dvh; }
|
||
header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; padding-top: max(8px, env(safe-area-inset-top)); background: var(--bar); border-bottom: 1px solid var(--line); }
|
||
header .name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
header .state { color: var(--dim); font-size: 13px; white-space: nowrap; display: flex; align-items: center; gap: 6px; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dim); flex: none; }
|
||
.dot.live { background: var(--ok); }
|
||
.dot.hold { background: var(--warn); }
|
||
.grow { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||
button { font: inherit; color: var(--text); background: #2b2f37; border: 1px solid var(--line); border-radius: 8px; padding: 6px 12px; cursor: pointer; white-space: nowrap; }
|
||
button.main { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
|
||
button.icon { padding: 6px 10px; }
|
||
#screen { flex: 1; min-height: 0; position: relative; background: #000; }
|
||
#screen > div { background: #000 !important; }
|
||
#msg { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; padding: 24px; color: var(--dim); pointer-events: none; }
|
||
#type { display: none; gap: 6px; padding: 8px 12px; padding-bottom: max(8px, env(safe-area-inset-bottom)); background: var(--bar); border-top: 1px solid var(--line); flex-wrap: wrap; }
|
||
#type.on { display: flex; }
|
||
#type input { flex: 1 1 160px; min-width: 0; font: inherit; color: var(--text); background: #111318; border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; }
|
||
@media (max-width: 420px) { header .hint { display: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<button class="icon" id="back" title="返回">←</button>
|
||
<div class="grow"><div class="name" id="name">Bot 的電腦</div><div class="state"><span class="dot" id="dot"></span><span id="state">連線中…</span></div></div>
|
||
<button id="kb" title="打字(任何語言)">打字</button>
|
||
<button class="main" id="take">接管</button>
|
||
</header>
|
||
<div id="screen"><div id="msg">連線中…</div></div>
|
||
<div id="type">
|
||
<input id="text" placeholder="要打的字(中文也可以),按送出" autocomplete="off" enterkeyhint="send">
|
||
<button class="main" id="send">送出</button>
|
||
<button data-k="Return">Enter</button><button data-k="Escape">Esc</button><button data-k="Tab">Tab</button><button data-k="BackSpace">⌫</button>
|
||
</div>
|
||
<script type="module">
|
||
import RFB from "./novnc.js";
|
||
|
||
// #host=…&name=…&ticket=… (the app's link) or #host=… with this site's sign-in (the phone page);
|
||
// #lan=1 on an AnyBot's own phone page. &control=1 starts with control (a bot asked for help).
|
||
const q = new URLSearchParams(location.hash.slice(1));
|
||
const name = q.get("name") || "Bot 的電腦";
|
||
const token = localStorage.getItem("anybot.token") || "";
|
||
const wsBase = (location.protocol === "https:" ? "wss://" : "ws://") + location.host;
|
||
const url = q.get("lan") === "1"
|
||
? `${wsBase}/api/vnc?t=${encodeURIComponent(token)}`
|
||
: `${wsBase}/relay/vnc/${encodeURIComponent(q.get("host") || "")}?` + (q.get("ticket") ? "ticket=" + encodeURIComponent(q.get("ticket")) : "t=" + encodeURIComponent(token));
|
||
const $ = (id) => document.getElementById(id);
|
||
document.title = name;
|
||
$("name").textContent = name;
|
||
|
||
let rfb = null, ws = null, control = q.get("control") === "1", tries = 0, connected = false;
|
||
const KEYS = { Return: 0xff0d, Escape: 0xff1b, Tab: 0xff09, BackSpace: 0xff08 };
|
||
|
||
function state(text, kind) { $("state").textContent = text; $("dot").className = "dot" + (kind ? " " + kind : ""); }
|
||
function message(text) { $("msg").textContent = text; $("msg").style.display = text ? "" : "none"; }
|
||
function showControl() {
|
||
$("take").textContent = control ? "交還給 Bot" : "接管";
|
||
$("take").className = control ? "" : "main";
|
||
$("kb").style.display = control ? "" : "none";
|
||
if (!control) $("type").classList.remove("on");
|
||
if (rfb) { rfb.viewOnly = !control; if (control) rfb.focus(); }
|
||
if (connected) state(control ? "你在操作(Bot 會等你停下)" : "即時觀看", control ? "hold" : "live");
|
||
}
|
||
|
||
function connect() {
|
||
message("連線中…"); state("連線中…");
|
||
ws = new WebSocket(url);
|
||
rfb = new RFB($("screen"), ws, { shared: true });
|
||
rfb.scaleViewport = true;
|
||
rfb.resizeSession = false;
|
||
rfb.qualityLevel = 6;
|
||
rfb.compressionLevel = 2;
|
||
rfb.showDotCursor = true;
|
||
rfb.viewOnly = !control;
|
||
rfb.addEventListener("connect", () => { tries = 0; connected = true; message(""); showControl(); });
|
||
rfb.addEventListener("disconnect", () => {
|
||
rfb = null; connected = false;
|
||
tries += 1;
|
||
state("連線中斷,正在重新連線…");
|
||
message(tries > 3 ? "連不上這台電腦。它的 AnyBot 可能關了,或連結已過期:回到 AnyBot 再打開一次。" : "連線中斷,正在重新連線…");
|
||
setTimeout(connect, Math.min(1000 * tries, 8000));
|
||
});
|
||
}
|
||
|
||
$("take").onclick = () => { control = !control; showControl(); };
|
||
$("kb").onclick = () => { $("type").classList.toggle("on"); if ($("type").classList.contains("on")) $("text").focus(); };
|
||
// Typed text goes over the same connection as words (any language), pasted in the bots' computer.
|
||
function send() { const t = $("text").value; if (!t || !ws || ws.readyState !== 1) return; ws.send(t); $("text").value = ""; }
|
||
$("send").onclick = send;
|
||
$("text").addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.isComposing) { e.preventDefault(); send(); } });
|
||
document.querySelectorAll("[data-k]").forEach((b) => b.onclick = () => { if (rfb) rfb.sendKey(KEYS[b.dataset.k], b.dataset.k); });
|
||
$("back").onclick = () => { if (history.length > 1) history.back(); else window.close(); };
|
||
|
||
showControl();
|
||
connect();
|
||
</script>
|
||
</body>
|
||
</html>
|