's'
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user