From bc01a405643e522fea6ae2f645e2e0870a2793e9 Mon Sep 17 00:00:00 2001 From: cryptocommuniums-afk Date: Sat, 14 Mar 2026 22:44:46 +0800 Subject: [PATCH] Improve mobile recorder focus mode --- client/src/pages/Recorder.tsx | 348 ++++++++++++++++++++++++++-------- tests/e2e/app.spec.ts | 17 +- 2 files changed, 276 insertions(+), 89 deletions(-) diff --git a/client/src/pages/Recorder.tsx b/client/src/pages/Recorder.tsx index a725a12..7c3971f 100644 --- a/client/src/pages/Recorder.tsx +++ b/client/src/pages/Recorder.tsx @@ -27,6 +27,8 @@ import { Download, FlipHorizontal, Loader2, + Maximize2, + Minimize2, MonitorUp, Scissors, ShieldAlert, @@ -95,7 +97,10 @@ function waitForIceGathering(peer: RTCPeerConnection) { } function isMobileDevice() { - return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); + return ( + /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || + window.matchMedia("(max-width: 768px)").matches + ); } function getArchiveProgress(session: MediaSession | null) { @@ -164,6 +169,7 @@ export default function Recorder() { const [mediaSession, setMediaSession] = useState(null); const [markers, setMarkers] = useState([]); const [connectionState, setConnectionState] = useState("new"); + const [immersivePreview, setImmersivePreview] = useState(false); const mobile = useMemo(() => isMobileDevice(), []); const mimeType = useMemo(() => pickRecorderMimeType(), []); @@ -712,7 +718,40 @@ export default function Recorder() { return; } playbackVideoRef.current.load(); - }, [currentPlaybackUrl, mode]); + }, [currentPlaybackUrl, immersivePreview, mode]); + + useEffect(() => { + if (mode === "archived") { + return; + } + + const liveVideo = liveVideoRef.current; + if (!liveVideo || !streamRef.current) { + return; + } + + if (liveVideo.srcObject !== streamRef.current) { + liveVideo.srcObject = streamRef.current; + } + liveVideo.play().catch(() => {}); + }, [cameraActive, immersivePreview, mode]); + + useEffect(() => { + if (!mobile) { + setImmersivePreview(false); + return; + } + + if (!immersivePreview) { + document.body.style.overflow = ""; + return; + } + + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = ""; + }; + }, [immersivePreview, mobile]); useEffect(() => { return () => { @@ -751,6 +790,156 @@ export default function Recorder() { const previewTitle = mode === "archived" ? "归档回放" : "实时取景"; const StatusIcon = statusBadge.icon; + const previewVideoClassName = immersivePreview && mobile + ? "h-full w-full object-contain" + : "h-full w-full object-contain"; + const railButtonClassName = + "h-16 w-16 flex-col gap-1 rounded-[20px] px-2 text-[11px] leading-tight shadow-lg shadow-black/20"; + + const renderPrimaryActions = (layout: "toolbar" | "rail" = "toolbar") => { + const rail = layout === "rail"; + const labelFor = (full: string, short: string) => (rail ? short : full); + const buttonClass = (tone: "default" | "outline" | "destructive" | "record" = "default") => { + if (!rail) { + switch (tone) { + case "outline": + return "h-12 rounded-2xl px-4"; + case "destructive": + return "h-12 rounded-2xl px-5"; + case "record": + return "h-12 rounded-2xl bg-red-600 px-5 hover:bg-red-700"; + default: + return "h-12 rounded-2xl px-5"; + } + } + + switch (tone) { + case "outline": + return `${railButtonClassName} border-white/15 bg-white/8 text-white hover:bg-white/14`; + case "destructive": + return `${railButtonClassName} bg-red-600 text-white hover:bg-red-700`; + case "record": + return `${railButtonClassName} bg-red-600 text-white hover:bg-red-700`; + default: + return `${railButtonClassName} bg-white text-slate-950 hover:bg-white/90`; + } + }; + + const iconClass = rail ? "h-5 w-5" : "mr-2 h-4 w-4"; + + return ( + <> + {mode === "idle" && ( + <> + {!cameraActive ? ( + + ) : ( + <> + + + + )} + {hasMultipleCameras && ( + + )} + + )} + + {(mode === "recording" || mode === "reconnecting") && ( + <> + + + + )} + + {mode === "archived" && ( + <> + {currentPlaybackUrl && ( + + )} + + + )} + + {mode === "finalizing" && ( + + )} + + ); + }; + + const renderPreviewMedia = () => ( + <> + {mode === "archived" && currentPlaybackUrl ? ( +