This commit is contained in:
eric
2026-03-28 16:27:42 -05:00
parent 6f79f259a1
commit 37ab25911a
16 changed files with 1176 additions and 330 deletions

View File

@@ -1,15 +1,6 @@
"use client";
import {
ArrowUpRight,
ChevronDown,
ChevronUp,
Copy,
Layers,
Loader2,
Package,
Terminal,
} from "lucide-react";
import { Copy, Layers, Loader2, Package, Terminal } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
type DeviceRow = { serial: string; model?: string; state: string };
@@ -58,8 +49,6 @@ type Props = {
devices: DeviceRow[];
adbAvailable: boolean;
scrcpyUrl: string;
androidShotUrl: string | null;
androidShotErr: string | null;
apkPath: string;
setApkPath: (v: string) => void;
onAndroidAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
@@ -76,8 +65,6 @@ export function LiveAndroidWorkstation({
devices,
adbAvailable,
scrcpyUrl,
androidShotUrl,
androidShotErr,
apkPath,
setApkPath,
onAndroidAction,
@@ -96,9 +83,11 @@ export function LiveAndroidWorkstation({
const [uiErr, setUiErr] = useState<string | null>(null);
const [openUrl, setOpenUrl] = useState("https://");
const [adbInputText, setAdbInputText] = useState("");
const [embedScrcpy, setEmbedScrcpy] = useState(false);
const [mirrorMode, setMirrorMode] = useState<"host" | "webusb">("host");
const [pending, setPending] = useState<string | null>(null);
const PANDA_WEB_SCRCPY = "https://pandatestgrid.github.io/panda-web-scrcpy/";
const lock = !!(busy || pending);
const loadOverview = useCallback(async () => {
@@ -280,6 +269,104 @@ export function LiveAndroidWorkstation({
) : null}
</div>
<div className="overflow-hidden rounded-2xl border border-violet-500/20 bg-gradient-to-b from-violet-500/[0.08] to-black/50">
<div className="flex flex-wrap gap-2 border-b border-white/10 px-3 py-2">
<button
type="button"
onClick={() => setMirrorMode("host")}
className={`rounded-lg px-3 py-2 text-[11px] font-semibold transition ${
mirrorMode === "host"
? "bg-violet-500/25 text-violet-100 ring-1 ring-violet-400/40"
: "text-zinc-500 hover:bg-white/5 hover:text-zinc-300"
}`}
>
{t("宿主投屏 (板子 ws-scrcpy)", "Host ws-scrcpy")}
</button>
<button
type="button"
onClick={() => setMirrorMode("webusb")}
className={`rounded-lg px-3 py-2 text-[11px] font-semibold transition ${
mirrorMode === "webusb"
? "bg-cyan-500/20 text-cyan-100 ring-1 ring-cyan-400/35"
: "text-zinc-500 hover:bg-white/5 hover:text-zinc-300"
}`}
>
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>
{mirrorMode === "host" ? (
<div>
<div className="space-y-2 border-b border-white/5 px-4 py-3">
<p className="text-[11px] 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.",
)}
</p>
<div className="flex flex-wrap items-center gap-2">
{adbAvailable && androidSerial ? (
<button
type="button"
disabled={lock}
onClick={() => void onAndroidAction("wake")}
className="rounded-lg bg-amber-500/20 px-3 py-1.5 text-[11px] text-amber-100 ring-1 ring-amber-500/30"
>
Wake
</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>
) : null}
</div>
</div>
{scrcpyUrl ? (
<iframe
title="ws-scrcpy"
src={scrcpyUrl}
className="h-[min(72vh,640px)] w-full bg-black"
allow="fullscreen; autoplay; clipboard-read; clipboard-write"
/>
) : (
<p className="px-4 py-8 text-center text-sm text-zinc-500">{t("未配置 ws-scrcpy 地址", "ws-scrcpy URL missing")}</p>
)}
</div>
) : (
<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: ",
)}
<a className="text-cyan-300 hover:underline" href={PANDA_WEB_SCRCPY} target="_blank" rel="noreferrer">
panda-web-scrcpy
</a>
</p>
<iframe
title="panda-web-scrcpy"
src={PANDA_WEB_SCRCPY}
className="h-[min(72vh,640px)] w-full bg-black"
allow="fullscreen; usb"
/>
</div>
)}
</div>
{adbAvailable && androidSerial ? (
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-2xl border border-violet-500/15 bg-zinc-950/50 p-5">
@@ -672,87 +759,25 @@ export function LiveAndroidWorkstation({
</div>
) : null}
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
<h3 className="text-sm font-semibold text-white">{t("安装 APK设备上的路径", "Install APK (path on device)")}</h3>
<p className="mt-1 text-xs text-zinc-500">
{t("请先用 adb push 将 apk 推到 /sdcard/ 等路径。", "Push APK to /sdcard/ via adb first.")}
</p>
<input
className="mt-4 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm"
value={apkPath}
onChange={(e) => setApkPath(e.target.value)}
/>
<button
type="button"
disabled={lock || !adbAvailable || !androidSerial}
onClick={() => void onAndroidAction("install_apk", { path: apkPath })}
className="mt-3 rounded-xl bg-emerald-500/20 px-4 py-2 text-sm font-medium text-emerald-100 ring-1 ring-emerald-500/30"
>
pm install
</button>
</div>
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-white">{t("实时画面ADB 截图)", "Live screen (ADB)")}</h3>
{scrcpyUrl ? (
<a
href={scrcpyUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-xs text-violet-300 hover:text-violet-200"
>
ws-scrcpy <ArrowUpRight className="h-3 w-3" />
</a>
) : null}
</div>
<p className="mt-1 text-xs text-zinc-500">
{t(
"约每 2.5 秒刷新;全帧低延迟请用 ws-scrcpy 新窗口或本页内嵌。",
"~2.5s ADB refresh; use ws-scrcpy for low-latency stream.",
)}
</p>
{!adbAvailable ? (
<p className="mt-3 text-sm text-amber-200/90">{t("ADB 未就绪,无法截图。", "ADB unavailable.")}</p>
) : null}
{androidShotErr && adbAvailable ? <p className="mt-3 text-sm text-rose-300">{androidShotErr}</p> : null}
{androidShotUrl ? (
<img
alt="Android live"
className="mt-4 max-h-[min(60vh,520px)] w-full rounded-xl border border-white/10 bg-black object-contain"
src={androidShotUrl}
{adbAvailable && androidSerial ? (
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
<h3 className="text-sm font-semibold text-white">{t("安装 APK设备上的路径", "Install APK (path on device)")}</h3>
<p className="mt-1 text-xs text-zinc-500">
{t("请先用 adb push 将 apk 推到 /sdcard/ 等路径。", "Push APK to /sdcard/ via adb first.")}
</p>
<input
className="mt-4 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm"
value={apkPath}
onChange={(e) => setApkPath(e.target.value)}
/>
) : null}
{adbAvailable && !androidShotUrl && !androidShotErr ? (
<p className="mt-6 text-center text-sm text-zinc-500">{t("加载截图…", "Loading screenshot…")}</p>
) : null}
</div>
{scrcpyUrl ? (
<div className="overflow-hidden rounded-2xl border border-white/[0.08] bg-black/40">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-white/5 px-4 py-3">
<button
type="button"
onClick={() => setEmbedScrcpy((v) => !v)}
className="flex items-center gap-2 text-xs font-medium text-zinc-300 hover:text-white"
>
ws-scrcpy
{embedScrcpy ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
<span className="font-normal text-zinc-600">
{embedScrcpy ? t("收起内嵌", "Collapse") : t("展开内嵌(省资源)", "Expand embed")}
</span>
</button>
<a href={scrcpyUrl} target="_blank" rel="noreferrer" className="text-xs text-violet-300">
{t("新窗口打开", "Open tab")}
</a>
</div>
{embedScrcpy ? (
<iframe title="scrcpy" src={scrcpyUrl} className="h-[min(56vh,420px)] w-full bg-black" />
) : (
<p className="px-4 py-3 text-center text-[11px] text-zinc-600">
{t("默认不加载 iframe避免占用带宽与解码需要时点「展开内嵌」。", "Iframe off by default to save resources.")}
</p>
)}
<button
type="button"
disabled={lock || !adbAvailable || !androidSerial}
onClick={() => void onAndroidAction("install_apk", { path: apkPath })}
className="mt-3 rounded-xl bg-emerald-500/20 px-4 py-2 text-sm font-medium text-emerald-100 ring-1 ring-emerald-500/30"
>
pm install
</button>
</div>
) : null}
</div>