This commit is contained in:
root
2026-05-17 13:21:44 +00:00
parent ffdc887b8a
commit a3e8a4a96c
27 changed files with 671 additions and 140 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import Image from "next/image";
import { type ChangeEvent, type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
import { type ChangeEvent, type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import OverviewCharts from "@/components/OverviewCharts";
import { ConfigAdvisorPanel } from "@/components/console/ConfigAdvisorPanel";
@@ -167,6 +167,41 @@ function parseResolution(resolution?: string) {
return { width: Number(match[1]), height: Number(match[2]) };
}
function clientPointToImagePixel(
img: HTMLImageElement,
clientX: number,
clientY: number,
): { x: number; y: number } | null {
const rect = img.getBoundingClientRect();
const nw = img.naturalWidth;
const nh = img.naturalHeight;
if (!nw || !nh || !rect.width || !rect.height) return null;
const elAR = rect.width / rect.height;
const imAR = nw / nh;
let drawW: number;
let drawH: number;
let offX: number;
let offY: number;
if (imAR > elAR) {
drawW = rect.width;
drawH = rect.width / imAR;
offX = 0;
offY = (rect.height - drawH) / 2;
} else {
drawH = rect.height;
drawW = rect.height * imAR;
offX = (rect.width - drawW) / 2;
offY = 0;
}
const lx = clientX - rect.left - offX;
const ly = clientY - rect.top - offY;
if (lx < 0 || ly < 0 || lx > drawW || ly > drawH) return null;
return {
x: Math.max(0, Math.min(nw - 1, Math.round((lx / drawW) * nw))),
y: Math.max(0, Math.min(nh - 1, Math.round((ly / drawH) * nh))),
};
}
function Button({
children,
className,
@@ -212,6 +247,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
const [error, setError] = useState<string | null>(null);
const [toast, setToast] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [pageVisible, setPageVisible] = useState(true);
const toastTimerRef = useRef<number | null>(null);
const [entries, setEntries] = useState<ProcessEntry[]>([]);
const [currentProcess, setCurrentProcess] = useState("");
@@ -280,7 +317,13 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
const notify = useCallback((message: string) => {
setToast(message);
window.setTimeout(() => setToast(null), 2500);
if (toastTimerRef.current != null) {
window.clearTimeout(toastTimerRef.current);
}
toastTimerRef.current = window.setTimeout(() => {
setToast(null);
toastTimerRef.current = null;
}, 2500);
}, []);
const fetchJson = useCallback(
@@ -481,6 +524,22 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
window.localStorage.setItem("live-console-theme", theme);
}, [theme, embeddedShellCrash]);
useEffect(() => {
const onVis = () => setPageVisible(document.visibilityState === "visible");
onVis();
document.addEventListener("visibilitychange", onVis);
return () => document.removeEventListener("visibilitychange", onVis);
}, []);
useEffect(
() => () => {
if (toastTimerRef.current != null) {
window.clearTimeout(toastTimerRef.current);
}
},
[],
);
useEffect(() => {
if (!embeddedShellCrash) return;
if (portalLang === "zh" || portalLang === "en") setLang(portalLang);
@@ -496,19 +555,19 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
}, [boot]);
useEffect(() => {
if (!currentProcess) return;
if (!currentProcess || !pageVisible) return;
void refreshProcess();
const timer = window.setInterval(() => void refreshProcess(), 1500);
return () => window.clearInterval(timer);
}, [currentProcess, refreshProcess]);
}, [currentProcess, pageVisible, refreshProcess]);
useEffect(() => {
if (booting || error) return;
if (booting || error || !pageVisible) return;
if (tab !== "overview" && tab !== "modules" && tab !== "shellcrash") return;
void refreshServices();
const timer = window.setInterval(() => void refreshServices(), 10000);
return () => clearInterval(timer);
}, [booting, error, refreshServices, tab]);
}, [booting, error, pageVisible, refreshServices, tab]);
useEffect(() => {
const visibleRoots = tab === "shellcrash" ? shellRoots : tab === "config" ? otherRoots : roots;
@@ -557,13 +616,15 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
}, [refreshAndroidDevices, refreshAndroidOverview, refreshAndroidPackages, refreshAndroidUi, tab]);
useEffect(() => {
if (tab !== "android" || !androidSerial) return;
if (tab !== "android" || !androidSerial || !pageVisible) return;
void refreshAndroidOverview(androidSerial);
setShotNonce(Date.now());
const timer = window.setInterval(() => {
void refreshAndroidOverview(androidSerial);
setShotNonce(Date.now());
}, 5000);
return () => window.clearInterval(timer);
}, [androidSerial, refreshAndroidOverview, tab]);
}, [androidSerial, pageVisible, refreshAndroidOverview, tab]);
useEffect(() => {
if (!androidSerial) {
@@ -773,17 +834,17 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
notify(tr("当前没有可用的 ADB 设备", "No active ADB device"));
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const sourceWidth = androidResolution?.width || event.currentTarget.naturalWidth || rect.width;
const sourceHeight = androidResolution?.height || event.currentTarget.naturalHeight || rect.height;
const x = Math.max(
0,
Math.min(sourceWidth, Math.round((event.clientX - rect.left) * (sourceWidth / rect.width))),
);
const y = Math.max(
0,
Math.min(sourceHeight, Math.round((event.clientY - rect.top) * (sourceHeight / rect.height))),
);
const point = clientPointToImagePixel(event.currentTarget, event.clientX, event.clientY);
if (!point) {
notify(tr("请点击画面有效区域", "Tap inside the visible frame"));
return;
}
const sourceWidth = androidResolution?.width || event.currentTarget.naturalWidth;
const sourceHeight = androidResolution?.height || event.currentTarget.naturalHeight;
const scaleX = sourceWidth > 0 ? sourceWidth / Math.max(1, event.currentTarget.naturalWidth) : 1;
const scaleY = sourceHeight > 0 ? sourceHeight / Math.max(1, event.currentTarget.naturalHeight) : 1;
const x = Math.max(0, Math.min(sourceWidth || point.x, Math.round(point.x * scaleX)));
const y = Math.max(0, Math.min(sourceHeight || point.y, Math.round(point.y * scaleY)));
setPreviewPoint({ x, y });
if (previewMode === "long_press") {
await sendAndroidAction("long_press", { x, y, duration: 750 });
@@ -843,7 +904,12 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
}
>
{toast ? (
<div className="animate-toast-in fixed bottom-24 left-1/2 z-50 max-w-[min(92vw,28rem)] -translate-x-1/2 rounded-2xl border border-cyan-500/25 bg-slate-950/95 px-5 py-3 text-center text-sm text-cyan-50 shadow-lg md:bottom-6 [html[data-theme=light]_&]:border-cyan-600/25 [html[data-theme=light]_&]:bg-white/95 [html[data-theme=light]_&]:text-cyan-950">
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="animate-toast-in fixed bottom-24 left-1/2 z-50 max-w-[min(92vw,28rem)] -translate-x-1/2 rounded-2xl border border-cyan-500/25 bg-slate-950/95 px-5 py-3 text-center text-sm text-cyan-50 shadow-lg md:bottom-6 [html[data-theme=light]_&]:border-cyan-600/25 [html[data-theme=light]_&]:bg-white/95 [html[data-theme=light]_&]:text-cyan-950"
>
{toast}
</div>
) : null}