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