This commit is contained in:
eric
2026-03-28 17:16:38 -05:00
parent 37ab25911a
commit 46fb194a02
20 changed files with 962 additions and 331 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { Copy, Layers, Loader2, Package, Terminal } from "lucide-react";
import { Copy, ExternalLink, Layers, Loader2, Package, RefreshCw, Terminal } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
type DeviceRow = { serial: string; model?: string; state: string };
@@ -84,9 +84,14 @@ export function LiveAndroidWorkstation({
const [openUrl, setOpenUrl] = useState("https://");
const [adbInputText, setAdbInputText] = useState("");
const [mirrorMode, setMirrorMode] = useState<"host" | "webusb">("host");
const [mirrorReloadKey, setMirrorReloadKey] = useState(0);
const [logcatLive, setLogcatLive] = useState(false);
const [pending, setPending] = useState<string | null>(null);
const PANDA_WEB_SCRCPY = "https://pandatestgrid.github.io/panda-web-scrcpy/";
const LINK_SCRCPY = "https://github.com/Genymobile/scrcpy";
const LINK_WS_SCRCPY = "https://github.com/NetrisTV/ws-scrcpy";
const LINK_PANDA_GH = "https://github.com/PandaTestGrid/panda-web-scrcpy";
const lock = !!(busy || pending);
@@ -157,26 +162,36 @@ export function LiveAndroidWorkstation({
}
};
const refreshLogcat = async () => {
if (!androidSerial) {
notify(t("请选择设备", "Pick a device"));
return;
}
const n = Math.min(3000, Math.max(1, parseInt(logcatLines, 10) || 200));
setPending("logcat");
setLogcatErr(null);
try {
const data = await fetchJson<{ text?: string }>(
`/android/logcat?serial=${encodeURIComponent(androidSerial)}&lines=${n}`,
);
setLogcatText(data.text ?? "");
} catch (e) {
setLogcatText("");
setLogcatErr((e as Error).message);
} finally {
setPending(null);
}
};
const refreshLogcat = useCallback(
async (opts?: { silent?: boolean }) => {
if (!androidSerial) {
return;
}
const n = Math.min(3000, Math.max(1, parseInt(logcatLines, 10) || 200));
const silent = !!opts?.silent;
if (!silent) setPending("logcat");
setLogcatErr(null);
try {
const data = await fetchJson<{ text?: string }>(
`/android/logcat?serial=${encodeURIComponent(androidSerial)}&lines=${n}`,
);
setLogcatText(data.text ?? "");
} catch (e) {
setLogcatText("");
setLogcatErr((e as Error).message);
} finally {
if (!silent) setPending(null);
}
},
[androidSerial, fetchJson, logcatLines],
);
useEffect(() => {
if (!logcatLive || !androidSerial || !adbAvailable) return;
const id = window.setInterval(() => void refreshLogcat({ silent: true }), 3000);
void refreshLogcat({ silent: true });
return () => clearInterval(id);
}, [logcatLive, androidSerial, adbAvailable, refreshLogcat]);
const loadUiNodes = async () => {
if (!androidSerial) {
@@ -260,10 +275,10 @@ export function LiveAndroidWorkstation({
))}
</div>
{adbAvailable && devices.length === 0 ? (
<p className="mt-4 rounded-lg border border-amber-500/20 bg-amber-500/5 p-3 text-xs text-amber-200/90">
<p className="mt-4 rounded-lg border border-amber-500/20 bg-amber-500/5 p-3 text-xs leading-relaxed text-amber-200/90">
{t(
"未检测到已授权设备:请 USB 连接并开启「USB 调试」,手机上允许本计算机调试。",
"No authorized device: enable USB debugging and authorize this computer.",
"未检测到已授权设备:双头 USB 接板子时请开「USB 调试」并授权;Redroid / 容器安卓可先 `adb connect IP:5555` 再刷新本页。",
"No device: enable USB debugging for dual-USB to the board; for Redroid use adb connect IP:5555 then refresh.",
)}
</p>
) : null}
@@ -293,22 +308,40 @@ export function LiveAndroidWorkstation({
>
WebUSB · Panda
</button>
<a
href="https://github.com/PandaTestGrid/panda-web-scrcpy"
target="_blank"
rel="noreferrer"
className="ml-auto self-center text-[11px] text-zinc-500 underline-offset-2 hover:text-violet-300 hover:underline"
>
GitHub
</a>
<div className="ml-auto flex flex-wrap items-center gap-2 self-center">
<a
href={LINK_SCRCPY}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[10px] text-zinc-500 hover:text-emerald-300"
>
scrcpy <ExternalLink className="h-3 w-3" />
</a>
<a
href={LINK_WS_SCRCPY}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[10px] text-zinc-500 hover:text-violet-300"
>
ws-scrcpy <ExternalLink className="h-3 w-3" />
</a>
<a
href={LINK_PANDA_GH}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[10px] text-zinc-500 hover:text-cyan-300"
>
panda-web-scrcpy <ExternalLink className="h-3 w-3" />
</a>
</div>
</div>
{mirrorMode === "host" ? (
<div>
<div className="space-y-2 border-b border-white/5 px-4 py-3">
<p className="text-[11px] text-zinc-400">
<p className="text-[11px] leading-relaxed text-zinc-400">
{t(
"手机接在 live 板子 USB 时使用本页:先在 ws-scrcpy 里选设备并连接;黑屏多为未连 WebSocket / 屏幕休眠——可先 Wake 再刷新或新窗口打开面板。",
"USB to the board: pick device in ws-scrcpy; black screen often means WS down or screen off—Wake, refresh, or open panel in a new tab.",
"板载 USB:投屏协议来自开源 scrcpy(见上方仓库链接);浏览器内常用 ws-scrcpy。黑屏时先 Wake,再点「重载投屏」或新窗口打开面板。",
"Board USB: scrcpy protocol; browser uses ws-scrcpy. Black screen: Wake, reload mirror, or open panel in new tab.",
)}
</p>
<div className="flex flex-wrap items-center gap-2">
@@ -323,19 +356,30 @@ export function LiveAndroidWorkstation({
</button>
) : null}
{scrcpyUrl ? (
<a
href={scrcpyUrl}
target="_blank"
rel="noreferrer"
className="rounded-lg border border-white/15 bg-black/30 px-3 py-1.5 text-[11px] text-violet-300"
>
{t("新窗口打开 :5000", "Open :5000 tab")}
</a>
<>
<button
type="button"
onClick={() => setMirrorReloadKey((k) => k + 1)}
className="inline-flex items-center gap-1 rounded-lg border border-white/15 bg-black/30 px-3 py-1.5 text-[11px] text-zinc-200"
>
<RefreshCw className="h-3.5 w-3.5" />
{t("重载投屏", "Reload mirror")}
</button>
<a
href={scrcpyUrl}
target="_blank"
rel="noreferrer"
className="rounded-lg border border-white/15 bg-black/30 px-3 py-1.5 text-[11px] text-violet-300"
>
{t("新窗口打开", "New tab")}
</a>
</>
) : null}
</div>
</div>
{scrcpyUrl ? (
<iframe
key={`ws-scrcpy-${mirrorReloadKey}`}
title="ws-scrcpy"
src={scrcpyUrl}
className="h-[min(72vh,640px)] w-full bg-black"
@@ -349,15 +393,15 @@ export function LiveAndroidWorkstation({
<div>
<p className="border-b border-white/5 px-4 py-3 text-[11px] leading-relaxed text-zinc-400">
{t(
"「Panda / WebUSB」在浏览器里直连手机:手机必须插在您正在用浏览器看页面的这台电脑的 USB 上(走 WebUSB,不是板子上的 ADB)。官方演示见 ",
"Panda uses WebUSB: phone must plug into the PC running this browser, not the board. Demo: ",
"Panda / WebUSB:手机必须插在您正在打开本页的电脑 USB 上(与板载 ADB 无关)。与官方 scrcpy 协议生态同源,适合本地调试。仓库:",
"Panda WebUSB: phone plugs into the PC running this browser. Same scrcpy-protocol family. Repo: ",
)}
<a className="text-cyan-300 hover:underline" href={PANDA_WEB_SCRCPY} target="_blank" rel="noreferrer">
panda-web-scrcpy
<a className="text-cyan-300 hover:underline" href={LINK_PANDA_GH} target="_blank" rel="noreferrer">
PandaTestGrid/panda-web-scrcpy
</a>
。
</p>
<iframe
key={`panda-${mirrorReloadKey}`}
title="panda-web-scrcpy"
src={PANDA_WEB_SCRCPY}
className="h-[min(72vh,640px)] w-full bg-black"
@@ -680,10 +724,25 @@ export function LiveAndroidWorkstation({
value={logcatLines}
onChange={(e) => setLogcatLines(e.target.value)}
/>
<label className="flex cursor-pointer items-center gap-2 text-[11px] text-zinc-400">
<input
type="checkbox"
className="rounded border-white/20 bg-black/40"
checked={logcatLive}
onChange={(e) => setLogcatLive(e.target.checked)}
/>
{t("每 3 秒自动刷新", "Auto every 3s")}
</label>
<button
type="button"
disabled={lock}
onClick={() => void refreshLogcat()}
onClick={() => {
if (!androidSerial) {
notify(t("请选择设备", "Pick a device"));
return;
}
void refreshLogcat();
}}
className="rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30"
>
{t("拉取日志", "Pull logs")}