"use client"; 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 }; type TFn = (zh: string, en: string) => string; type AndroidOverview = { serial?: string; model?: string; brand?: string; android_version?: string; sdk?: string; battery?: string; focus?: string; resolution?: string; rotation?: string; screen_state?: string; }; type PkgRow = { package: string; label?: string }; type UiNode = { label: string; text?: string; content_desc?: string; resource_id?: string; center_x: number; center_y: number; clickable?: boolean; enabled?: boolean; }; const SHELL_SNIPPETS: { label: string; cmd: string }[] = [ { label: "getprop", cmd: "getprop ro.build.version.release" }, { label: "top pkg", cmd: "dumpsys activity activities | grep -E 'mResumedActivity' | tail -n 1" }, { label: "sdcard", cmd: "ls -la /sdcard | head -n 24" }, { label: "net", cmd: "dumpsys connectivity | head -n 40" }, ]; type Props = { t: TFn; notify: (msg: string) => void; fetchJson: (path: string, init?: RequestInit) => Promise; /** 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) => Promise; onCopy: (text: string) => void; }; export function LiveAndroidWorkstation({ t, notify, fetchJson, busy, androidSerial, setAndroidSerial, devices, adbAvailable, scrcpyUrl, apkPath, setApkPath, onAndroidAction, onCopy, }: Props) { const [overview, setOverview] = useState(null); const [overviewErr, setOverviewErr] = useState(null); const [pkgQuery, setPkgQuery] = useState(""); const [pkgList, setPkgList] = useState([]); const [shellCmd, setShellCmd] = useState("getprop ro.product.model"); const [shellOut, setShellOut] = useState(""); const [logcatLines, setLogcatLines] = useState("300"); const [logcatText, setLogcatText] = useState(""); const [logcatErr, setLogcatErr] = useState(null); const [uiNodes, setUiNodes] = useState([]); const [uiErr, setUiErr] = useState(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(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 loadOverview = useCallback(async () => { if (!androidSerial || !adbAvailable) { setOverview(null); setOverviewErr(null); return; } setPending("overview"); setOverviewErr(null); try { const o = await fetchJson( `/android/overview?serial=${encodeURIComponent(androidSerial)}`, ); setOverview(o); } catch (e) { setOverview(null); setOverviewErr((e as Error).message); } finally { setPending(null); } }, [adbAvailable, androidSerial, fetchJson]); useEffect(() => { void loadOverview(); }, [loadOverview]); const searchPackages = async () => { if (!androidSerial) { notify(t("请选择设备", "Pick a device")); return; } setPending("packages"); try { const q = encodeURIComponent(pkgQuery.trim()); const data = await fetchJson<{ packages: PkgRow[] }>( `/android/packages?serial=${encodeURIComponent(androidSerial)}&query=${q}&limit=80`, ); setPkgList(data.packages || []); } catch (e) { notify((e as Error).message); setPkgList([]); } finally { setPending(null); } }; const runShell = async () => { if (!androidSerial) { notify(t("请选择设备", "Pick a device")); return; } const cmd = shellCmd.trim(); if (!cmd) return; setPending("shell"); try { const data = await fetchJson<{ output?: string; message?: string }>("/android/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "shell", serial: androidSerial, payload: { command: cmd } }), }); setShellOut(data.output ?? data.message ?? ""); } catch (e) { setShellOut((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) { notify(t("请选择设备", "Pick a device")); return; } setPending("ui"); setUiErr(null); try { const data = await fetchJson<{ nodes: UiNode[] }>( `/android/ui?serial=${encodeURIComponent(androidSerial)}&limit=100`, ); setUiNodes(data.nodes || []); } catch (e) { setUiNodes([]); setUiErr((e as Error).message); } finally { setPending(null); } }; const tapNode = async (x: number, y: number) => { await onAndroidAction("tap", { x, y }); }; const sendKey = async (keycode: string) => { await onAndroidAction("key", { keycode }); }; const openUrlGo = async () => { const u = openUrl.trim(); if (!u || u === "https://") { notify(t("填写 URL", "Enter URL")); return; } await onAndroidAction("open_url", { url: u }); }; const sendAdbText = async () => { const raw = adbInputText.trim(); if (!raw) { notify(t("填写要输入的文字", "Enter text")); return; } await onAndroidAction("text", { text: raw }); }; return (

{t("设备与工作台", "Device workstation")}

{!adbAvailable ? ( {t("宿主机未检测到 adb", "adb not found on host")} ) : ( )}
{devices.map((d) => ( ))}
{adbAvailable && devices.length === 0 ? (

{t( "未检测到已授权设备:双头 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.", )}

) : null}
{mirrorMode === "host" ? (

{t( "板载 USB:投屏协议来自开源 scrcpy(见上方仓库链接);浏览器内常用 ws-scrcpy。黑屏时先 Wake,再点「重载投屏」或新窗口打开面板。", "Board USB: scrcpy protocol; browser uses ws-scrcpy. Black screen: Wake, reload mirror, or open panel in new tab.", )}

{adbAvailable && androidSerial ? ( ) : null} {scrcpyUrl ? ( <> {t("新窗口打开", "New tab")} ) : null}
{scrcpyUrl ? (