This commit is contained in:
eric
2026-03-29 02:33:37 -05:00
parent 16dc9abbbb
commit ef87c43b33
17 changed files with 1197 additions and 407 deletions

View File

@@ -1,9 +1,11 @@
"use client";
import { Copy, ExternalLink, Layers, Loader2, Package, RefreshCw, Terminal } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Copy, Layers, Loader2, Package, Terminal } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
type DeviceRow = { serial: string; model?: string; state: string };
import { LiveAndroidStreamConsole } from "@/components/live-product/LiveAndroidStreamConsole";
type DeviceRow = { serial: string; model?: string; state: string; transport?: string };
type TFn = (zh: string, en: string) => string;
type AndroidOverview = {
@@ -42,33 +44,35 @@ type Props = {
t: TFn;
notify: (msg: string) => void;
fetchJson: <T,>(path: string, init?: RequestInit) => Promise<T>;
apiBase: string;
/** Parent busy (e.g. adb actions); combined with local pending for UI lock. */
busy: string | null;
androidSerial: string;
setAndroidSerial: (s: string) => void;
devices: DeviceRow[];
adbAvailable: boolean;
scrcpyUrl: string;
apkPath: string;
setApkPath: (v: string) => void;
onAndroidAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
onCopy: (text: string) => void;
onRefreshDevices: () => void;
};
export function LiveAndroidWorkstation({
t,
notify,
fetchJson,
apiBase,
busy,
androidSerial,
setAndroidSerial,
devices,
adbAvailable,
scrcpyUrl,
apkPath,
setApkPath,
onAndroidAction,
onCopy,
onRefreshDevices,
}: Props) {
const [overview, setOverview] = useState<AndroidOverview | null>(null);
const [overviewErr, setOverviewErr] = useState<string | null>(null);
@@ -83,30 +87,11 @@ export function LiveAndroidWorkstation({
const [uiErr, setUiErr] = useState<string | null>(null);
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);
const hostMirrorSrc = useMemo(() => {
if (!scrcpyUrl) return "";
try {
const u = new URL(scrcpyUrl);
u.searchParams.set("_lh", String(mirrorReloadKey));
return u.toString();
} catch {
const sep = scrcpyUrl.includes("?") ? "&" : "?";
return `${scrcpyUrl}${sep}_lh=${mirrorReloadKey}`;
}
}, [scrcpyUrl, mirrorReloadKey]);
const loadOverview = useCallback(async () => {
if (!androidSerial || !adbAvailable) {
setOverview(null);
@@ -281,7 +266,14 @@ export function LiveAndroidWorkstation({
: "border-white/10 bg-black/30 text-zinc-400"
}`}
>
<div className="font-medium text-zinc-200">{d.model || d.serial}</div>
<div className="flex items-center gap-2">
<span className="font-medium text-zinc-200">{d.model || d.serial}</span>
{d.transport ? (
<span className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-[9px] uppercase text-zinc-500">
{d.transport}
</span>
) : null}
</div>
<div className="text-[10px] text-zinc-600">{d.serial}</div>
</button>
))}
@@ -296,134 +288,20 @@ 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>
<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] leading-relaxed text-zinc-400">
{t(
"板载 ws-scrcpy 同时只允许一个浏览器会话:多标签/总览与安卓页各嵌一个 iframe 会互相抢连接。请只保留一个投屏页,或反复点「重载投屏」。已打补丁的安装会踢掉旧连接以便嵌入。黑屏请先 Wake会连发唤醒+滑动+Home。",
"Only one ws-scrcpy browser session: multiple embedded iframes fight for the stream—keep one tab or use Reload. Patched installs replace the old session. Black screen: try Wake (wake+swipe+home), then Reload or 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 ? (
<>
<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={hostMirrorSrc}
className="h-[min(72vh,640px)] w-full bg-black"
allow="fullscreen; autoplay; clipboard-read; clipboard-write"
referrerPolicy="no-referrer-when-downgrade"
loading="eager"
/>
) : (
<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 上(与板载 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={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"
allow="fullscreen; usb"
/>
</div>
)}
</div>
<LiveAndroidStreamConsole
t={t}
apiBase={apiBase}
fetchJson={fetchJson}
serial={androidSerial}
devices={devices}
setSerial={setAndroidSerial}
adbAvailable={adbAvailable}
lock={lock}
overview={overview}
onAndroidAction={onAndroidAction}
notify={notify}
onRefreshDevices={onRefreshDevices}
/>
{adbAvailable && androidSerial ? (
<div className="grid gap-4 lg:grid-cols-2">