| 1 | import { useEffect, useState } from "react"; |
| 2 | import type { DesktopBrowserHost } from "../lib/browserHost"; |
| 3 | import { useBrowserPanelStore } from "../lib/browserPanelStore"; |
| 4 | import { useI18n } from "../lib/i18n"; |
| 5 | |
| 6 | type Recording = Awaited<ReturnType<NonNullable<DesktopBrowserHost["record"]>>>; |
| 7 | export function BrowserEvidenceControls({ tabId }: { tabId: string }) { |
| 8 | const chinese = useI18n().locale !== "en"; |
| 9 | const [recording, setRecording] = useState<Recording>(null); |
| 10 | const [busy, setBusy] = useState(false); |
| 11 | const [diagnostics, setDiagnostics] = useState<string | null>(null); |
| 12 | const [capture, setCapture] = useState<{ path: string; width: number; height: number } | null>(null); |
| 13 | useEffect(() => { |
| 14 | let disposed = false; |
| 15 | const poll = async () => { |
| 16 | try { const result = await useBrowserPanelStore.getState().host?.record?.(tabId, "status"); if (!disposed) setRecording(result ?? null); } |
| 17 | catch { /* Availability is reported by the explicit action. */ } |
| 18 | }; |
| 19 | void poll(); const timer = setInterval(() => void poll(), 1000); |
| 20 | return () => { disposed = true; clearInterval(timer); }; |
| 21 | }, [tabId]); |
| 22 | const active = recording && ["preparing", "recording", "finalizing"].includes(recording.state); |
| 23 | const run = async (action: "start" | "stop" | "cancel") => { |
| 24 | const { host, notify } = useBrowserPanelStore.getState(); |
| 25 | if (!host?.record) { notify(chinese ? "当前宿主不支持录制" : "Recording is unavailable"); return; } |
| 26 | setBusy(true); |
| 27 | try { setRecording(await host.record(tabId, action)); } catch (error) { notify(String(error)); } |
| 28 | finally { setBusy(false); } |
| 29 | }; |
| 30 | return <div className="browser-evidence"> |
| 31 | <button type="button" className="btn btn--small" onClick={() => { |
| 32 | const { host, notify } = useBrowserPanelStore.getState(); |
| 33 | void host?.screenshot?.(tabId).then(setCapture).catch(error => notify(String(error))); |
| 34 | }}>{chinese ? "截图" : "Screenshot"}</button> |
| 35 | {capture && <button type="button" className="btn btn--small" title={capture.path} onClick={() => { void navigator.clipboard.writeText(capture.path); }}>{capture.width}×{capture.height} · {chinese ? "复制路径" : "Copy path"}</button>} |
| 36 | <button type="button" className="btn btn--small" title={chinese ? "关闭面板后也可通过 View 菜单或 Ctrl/⌘+Shift+R 停止录制" : "Stop from the View menu or Ctrl/⌘+Shift+R even when this panel is closed"} disabled={busy} onClick={() => void run(active ? "stop" : "start")}>{active ? (chinese ? "停止录制" : "Stop recording") : (chinese ? "录制 · 20 秒" : "Record · 20 s")}</button> |
| 37 | {active && <button type="button" className="btn btn--small" onClick={() => void run("cancel")}>{chinese ? "取消录制" : "Cancel recording"}</button>} |
| 38 | {recording && <span role="status" title={recording.path ?? recording.error}>{recording.state} · {Math.ceil(recording.bytes / 1024)} KiB</span>} |
| 39 | {recording?.path && <button type="button" className="btn btn--small" onClick={() => { void navigator.clipboard.writeText(recording.path!); }}>{chinese ? "复制产物路径" : "Copy artifact path"}</button>} |
| 40 | <button type="button" className="btn btn--small" onClick={() => { |
| 41 | const { host, notify } = useBrowserPanelStore.getState(); |
| 42 | void host?.diagnostics?.(tabId).then(result => setDiagnostics(result.available ? JSON.stringify(result.entries ?? [], null, 2) : (chinese ? "诊断不可用" : "Diagnostics unavailable"))).catch(error => notify(String(error))); |
| 43 | }}>{chinese ? "页面诊断" : "Page diagnostics"}</button> |
| 44 | {diagnostics !== null && <details open><summary onClick={() => setDiagnostics(null)}>{chinese ? "关闭诊断" : "Close diagnostics"}</summary><pre>{diagnostics}</pre></details>} |
| 45 | </div>; |
| 46 | } |
| 47 |