// Floating recording overlay - Developer-focused minimal UI import { useState, useEffect, useCallback, useMemo } from "react"; import { emitTo } from "@tauri-apps/api/event"; import { enumerateDisplays, enumerateWindows, startRecording, startWindowRecording, startRegionRecording, stopRecording, onToggleRecordingHotkey, hideRecordingOverlay, resizeRecordingOverlay, showRegionSelector, hideRegionSelector, onRegionSelected, onRegionCancelled, recordStepMarker, onMarkerHotkey, type DisplayDevice, type WindowInfo, type RegionBounds, } from "../lib/api"; import { trackWorkflowEvent } from "../lib/usageMetrics"; type RecordingMode = "display" | "window" | "region" | "terminal"; // Styles using design tokens const styles = { container: { width: "100%", height: "100%", background: "rgba(9, 9, 11, 0.92)", backdropFilter: "blur(24px)", WebkitBackdropFilter: "blur(24px)", borderRadius: "var(--radius-xl)", border: "1px solid var(--border-default)", display: "flex", alignItems: "center", justifyContent: "center", padding: "0 var(--space-4)", boxSizing: "border-box" as const, userSelect: "none" as const, cursor: "default", fontFamily: "var(--font-sans)", boxShadow: "var(--shadow-xl)", }, countdown: { width: "100%", height: "100%", background: "rgba(9, 9, 11, 0.95)", backdropFilter: "blur(24px)", borderRadius: "var(--radius-xl)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-primary)", fontSize: "2.5rem", fontWeight: "var(--weight-bold)" as unknown as number, fontFamily: "var(--font-mono)", userSelect: "none" as const, letterSpacing: "var(--tracking-tight)", }, countdownNumber: { animation: "countdown-pop 0.5s ease-out", }, recordingState: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: "var(--space-3)", }, timerSection: { display: "flex", alignItems: "center", gap: "var(--space-2)", }, recordingDot: { width: "10px", height: "10px", borderRadius: "50%", backgroundColor: "var(--accent-record)", boxShadow: "0 0 8px var(--accent-record-glow)", }, timer: { color: "var(--text-primary)", fontSize: "var(--text-md)", fontWeight: "var(--weight-semibold)" as unknown as number, fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", letterSpacing: "0.05em", }, buttonGroup: { display: "flex", gap: "var(--space-2)", }, stopBtn: { width: "34px", height: "34px", borderRadius: "var(--radius-md)", border: "none", background: "var(--accent-record)", color: "white", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", transition: "var(--transition-all)", boxShadow: "0 0 12px var(--accent-record-glow)", }, secondaryBtn: { width: "34px", height: "34px", borderRadius: "var(--radius-md)", border: "1px solid var(--border-strong)", background: "var(--bg-elevated)", color: "var(--text-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", transition: "var(--transition-all)", }, preRecordingState: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: "var(--space-3)", }, modeButtons: { display: "flex", gap: "var(--space-1)", }, modeBtn: { width: "36px", height: "36px", borderRadius: "var(--radius-md)", border: "1px solid transparent", background: "transparent", color: "var(--text-tertiary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "var(--text-md)", transition: "var(--transition-all)", }, modeBtnActive: { border: "1px solid var(--accent-primary)", background: "var(--accent-primary-muted)", color: "var(--accent-primary-hover)", }, windowPicker: { position: "relative" as const, flex: 1, minWidth: 0, }, windowPickerBtn: { width: "100%", padding: "var(--space-2) var(--space-3)", borderRadius: "var(--radius-md)", border: "1px solid var(--border-strong)", background: "var(--bg-elevated)", color: "var(--text-primary)", cursor: "pointer", fontSize: "var(--text-xs)", textAlign: "left" as const, display: "flex", alignItems: "center", justifyContent: "space-between", gap: "var(--space-2)", overflow: "hidden", }, windowPickerText: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" as const, }, dropdown: { position: "absolute" as const, top: "100%", left: 0, right: 0, marginTop: "var(--space-1)", background: "var(--bg-surface)", border: "1px solid var(--border-strong)", borderRadius: "var(--radius-lg)", maxHeight: "200px", overflowY: "auto" as const, zIndex: "var(--z-dropdown)", boxShadow: "var(--shadow-lg)", }, dropdownEmpty: { padding: "var(--space-3)", color: "var(--text-tertiary)", fontSize: "var(--text-xs)", textAlign: "center" as const, }, dropdownItem: { width: "100%", padding: "var(--space-3)", border: "none", background: "transparent", color: "var(--text-primary)", cursor: "pointer", fontSize: "var(--text-xs)", textAlign: "left" as const, display: "block", borderBottom: "1px solid var(--border-subtle)", transition: "var(--transition-colors)", }, dropdownItemTitle: { fontWeight: "var(--weight-medium)" as unknown as number, }, dropdownItemSubtitle: { color: "var(--text-tertiary)", fontSize: "10px", marginTop: "2px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" as const, }, windowPanel: { marginTop: "var(--space-2)", background: "var(--bg-surface)", border: "1px solid var(--border-strong)", borderRadius: "var(--radius-lg)", padding: "var(--space-2)", boxShadow: "var(--shadow-lg)", maxHeight: "240px", overflow: "auto" as const, }, windowGrid: { display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: "var(--space-2)", }, windowCard: { border: "1px solid var(--border-subtle)", borderRadius: "var(--radius-lg)", background: "rgba(255,255,255,0.02)", overflow: "hidden", cursor: "pointer", transition: "var(--transition-all)", textAlign: "left" as const, padding: 0, }, windowCardSelected: { border: "1px solid var(--accent-primary)", boxShadow: "0 0 0 1px var(--accent-primary) inset", background: "var(--accent-primary-muted)", }, windowThumb: { height: "72px", background: "linear-gradient(135deg, rgba(99,102,241,0.14) 0%, rgba(14,165,233,0.10) 45%, rgba(255,255,255,0.03) 100%)", position: "relative" as const, }, windowChrome: { position: "absolute" as const, top: 0, left: 0, right: 0, height: "16px", background: "rgba(0,0,0,0.28)", borderBottom: "1px solid rgba(255,255,255,0.06)", display: "flex", alignItems: "center", padding: "0 8px", gap: "6px", }, windowDot: { width: "6px", height: "6px", borderRadius: "50%", background: "rgba(255,255,255,0.28)", }, windowCardBody: { padding: "10px 10px 10px", minWidth: 0, }, windowCardApp: { color: "var(--text-secondary)", fontSize: "10px", letterSpacing: "0.02em", textTransform: "uppercase" as const, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" as const, }, windowCardTitle: { color: "var(--text-primary)", fontSize: "var(--text-xs)", marginTop: "2px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" as const, }, windowCardMeta: { color: "var(--text-tertiary)", fontSize: "10px", marginTop: "4px", fontFamily: "var(--font-mono)", opacity: 0.9, }, windowCardCheck: { position: "absolute" as const, right: "8px", bottom: "8px", width: "22px", height: "22px", borderRadius: "999px", background: "var(--accent-primary)", color: "white", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 6px 18px rgba(0,0,0,0.35)", }, regionInfo: { flex: 1, display: "flex", alignItems: "center", gap: "var(--space-2)", }, regionBadge: { padding: "var(--space-2) var(--space-3)", borderRadius: "var(--radius-md)", background: "var(--accent-primary-muted)", border: "1px solid var(--accent-primary)", color: "var(--accent-primary-hover)", fontSize: "var(--text-xs)", fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", }, regionBtn: { padding: "var(--space-2) var(--space-3)", borderRadius: "var(--radius-md)", border: "1px solid var(--border-strong)", background: "var(--bg-elevated)", color: "var(--text-primary)", cursor: "pointer", fontSize: "var(--text-xs)", transition: "var(--transition-colors)", }, recordBtn: { height: "40px", minWidth: "154px", padding: "0 16px", borderRadius: "12px", border: "1px solid color-mix(in srgb, #ffffff 18%, transparent)", background: "linear-gradient(180deg, #ff4f5f 0%, #e83444 52%, #cf2232 100%)", color: "white", cursor: "pointer", fontSize: "15px", fontWeight: 760, letterSpacing: "0.01em", display: "flex", alignItems: "center", justifyContent: "center", gap: "10px", whiteSpace: "nowrap" as const, boxShadow: "0 10px 24px rgba(232, 52, 68, 0.32), inset 0 1px 0 rgba(255,255,255,0.24)", transition: "transform 140ms var(--ease-out), box-shadow 140ms var(--ease-out)", }, recordBtnDotShell: { width: "22px", height: "22px", borderRadius: "999px", background: "rgba(255, 255, 255, 0.16)", border: "1px solid rgba(255, 255, 255, 0.22)", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "inset 0 1px 0 rgba(255,255,255,0.18)", }, error: { position: "absolute" as const, bottom: "-44px", left: "50%", transform: "translateX(-50%)", background: "var(--accent-record)", color: "white", padding: "var(--space-2) var(--space-4)", borderRadius: "var(--radius-lg)", fontSize: "var(--text-xs)", whiteSpace: "nowrap" as const, boxShadow: "var(--shadow-md)", animation: "fade-in-up 0.2s ease-out", }, closeBtn: { width: "28px", height: "28px", borderRadius: "var(--radius-md)", border: "none", background: "transparent", color: "var(--text-muted)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "var(--text-xs)", transition: "var(--transition-colors)", }, boxShadow: "var(--shadow-md)", animation: "fade-in-up 0.2s ease-out", markerFeedback: { position: "absolute" as const, top: "-40px", left: "50%", transform: "translateX(-50%)", background: "var(--bg-elevated)", border: "1px solid var(--border-strong)", color: "var(--text-primary)", padding: "var(--space-2) var(--space-4)", borderRadius: "var(--radius-full)", fontSize: "var(--text-xs)", fontWeight: "var(--weight-medium)" as unknown as number, display: "flex", alignItems: "center", gap: "var(--space-2)", boxShadow: "var(--shadow-lg)", animation: "slide-down-fade 0.3s ease-out", zIndex: 100, }, stepBtn: { height: "34px", padding: "0 var(--space-3)", borderRadius: "var(--radius-md)", border: "1px solid var(--border-strong)", background: "var(--bg-elevated)", color: "var(--text-primary)", cursor: "pointer", display: "flex", alignItems: "center", gap: "var(--space-2)", fontSize: "var(--text-xs)", fontWeight: "var(--weight-medium)" as unknown as number, transition: "var(--transition-all)", }, }; // SVG Icons const icons = { display: ( ), window: ( ), region: ( ), terminal: ( ), stop: ( ), restart: ( ), close: ( ), record: ( ), chevronUp: ( ), chevronDown: ( ), plus: ( ), check: ( ), library: ( ), }; function OverlayView() { const [recording, setRecording] = useState(false); const [stopping, setStopping] = useState(false); const [elapsedTime, setElapsedTime] = useState(0); const [countdown, setCountdown] = useState(null); const [mode, setMode] = useState("display"); const [displays, setDisplays] = useState([]); const [selectedDisplay, setSelectedDisplay] = useState(null); const [windows, setWindows] = useState([]); const [selectedWindow, setSelectedWindow] = useState(null); const [windowPickerOpen, setWindowPickerOpen] = useState(false); const [selectedRegion, setSelectedRegion] = useState(null); const [error, setError] = useState(null); const [markerFeedback, setMarkerFeedback] = useState(null); // Load displays on mount useEffect(() => { async function loadDisplays() { try { const displayList = await enumerateDisplays(); setDisplays(displayList); const primary = displayList.find((d) => d.is_primary); if (primary) { setSelectedDisplay(primary.id); } else if (displayList.length > 0) { setSelectedDisplay(displayList[0].id); } } catch (err) { console.error("Failed to enumerate displays:", err); } } loadDisplays(); }, []); // Load windows when window mode is selected useEffect(() => { async function loadWindows() { if (mode !== "window") return; try { const windowList = await enumerateWindows(); setWindows(windowList); if (!selectedWindow && windowList.length > 0) { setSelectedWindow(windowList[0]); } setWindowPickerOpen(true); } catch (err) { console.error("Failed to enumerate windows:", err); setError("Failed to get window list"); } } loadWindows(); }, [mode]); const overlayExpanded = useMemo(() => { return !recording && mode === "window" && windowPickerOpen; }, [recording, mode, windowPickerOpen]); // Expand the overlay while the window picker is open so the menu isn't clipped by the WebView. useEffect(() => { if (recording) return; resizeRecordingOverlay(false, overlayExpanded).catch(() => { // Best-effort: if resize fails, keep UI functional. }); }, [recording, overlayExpanded]); // Listen for region selection events useEffect(() => { let unlistenSelected: (() => void) | undefined; let unlistenCancelled: (() => void) | undefined; const setupListeners = async () => { unlistenSelected = await onRegionSelected((region) => { console.log("Region selected:", region); setSelectedRegion(region); setMode("region"); }); unlistenCancelled = await onRegionCancelled(() => { console.log("Region selection cancelled"); }); }; setupListeners(); return () => { if (unlistenSelected) unlistenSelected(); if (unlistenCancelled) unlistenCancelled(); }; }, []); // Timer logic useEffect(() => { let interval: NodeJS.Timeout; if (recording) { interval = setInterval(() => { setElapsedTime((prev) => prev + 1); }, 1000); } else { setElapsedTime(0); } return () => clearInterval(interval); }, [recording]); // Countdown logic useEffect(() => { if (countdown === null) return; if (countdown === 0) { setCountdown(null); doStartRecording(); return; } const timer = setTimeout(() => { setCountdown(countdown - 1); }, 1000); return () => clearTimeout(timer); }, [countdown]); const initiateCountdown = useCallback(() => { if (mode === "display" && !selectedDisplay) { setError("No display selected"); return; } if (mode === "window" && !selectedWindow) { setError("No window selected"); return; } if (mode === "region" && !selectedRegion) { setError("No region selected"); return; } setError(null); setWindowPickerOpen(false); setCountdown(3); }, [mode, selectedDisplay, selectedWindow, selectedRegion]); const doStartRecording = useCallback(async () => { try { let startedSessionId: string | null = null; if (mode === "region") { if (!selectedRegion) { setError("No region selected"); return; } startedSessionId = await startRegionRecording( selectedRegion.x, selectedRegion.y, selectedRegion.width, selectedRegion.height ); } else if (mode === "window") { if (!selectedWindow) { setError("No window selected"); return; } startedSessionId = await startWindowRecording( selectedWindow.id, Math.round(selectedWindow.bounds.width), Math.round(selectedWindow.bounds.height) ); } else { if (!selectedDisplay) { setError("No display selected"); return; } const display = displays.find((d) => d.id === selectedDisplay); if (!display) { setError("Display not found"); return; } startedSessionId = await startRecording( display.id, display.resolution.width, display.resolution.height ); } if (startedSessionId) { trackWorkflowEvent("recording_started", { sessionId: startedSessionId, metadata: { surface: "overlay", mode }, }); } setRecording(true); await resizeRecordingOverlay(true); } catch (err) { setError(`Failed to start: ${err}`); } }, [mode, selectedDisplay, displays, selectedWindow, selectedRegion]); const regionBadgeText = (() => { if (!selectedRegion) return ""; const w = selectedRegion.logicalWidth ?? selectedRegion.width; const h = selectedRegion.logicalHeight ?? selectedRegion.height; const suffix = selectedRegion.logicalWidth != null && selectedRegion.scaleFactor != null && selectedRegion.scaleFactor !== 1 ? ` @${selectedRegion.scaleFactor}x` : ""; return `${Math.round(w)} x ${Math.round(h)}${suffix}`; })(); const handleStopRecording = useCallback(async () => { setRecording(false); setStopping(true); try { const sid = await stopRecording(); trackWorkflowEvent("recording_stopped", { sessionId: sid, metadata: { surface: "overlay", reason: "stop" }, }); // Best-effort: hide the region frame after recording so it doesn't linger on screen. try { await hideRegionSelector(); } catch { // ignore } const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); const mainWindow = await WebviewWindow.getByLabel("main"); if (mainWindow) { await mainWindow.show(); await mainWindow.setFocus(); } else { setError("Main window is unavailable. Use tray menu: Open Library."); return; } await emitTo("main", "navigate-to-editor", { sessionId: sid }); await hideRecordingOverlay(); } catch (err) { setError(`Failed to stop: ${err}`); } finally { setStopping(false); } }, []); const handleOpenLibrary = useCallback(async () => { if (recording || stopping) return; try { const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); const mainWindow = await WebviewWindow.getByLabel("main"); if (mainWindow) { await mainWindow.show(); await mainWindow.setFocus(); } else { setError("Main window is unavailable. Use tray menu: Open Library."); return; } await emitTo("main", "navigate-to-library", {}); await hideRecordingOverlay(); } catch (err) { setError(`Failed to open library: ${err}`); } }, [recording, stopping]); const handleCancel = useCallback(async () => { setStopping(true); try { const sid = await stopRecording(); trackWorkflowEvent("recording_stopped", { sessionId: sid, metadata: { surface: "overlay", reason: "cancel" }, }); setRecording(false); try { await hideRegionSelector(); } catch { // ignore } await hideRecordingOverlay(); } catch (err) { console.error("Cancel failed:", err); } finally { setStopping(false); } }, []); const handleRestart = useCallback(async () => { setStopping(true); try { const sid = await stopRecording(); trackWorkflowEvent("recording_stopped", { sessionId: sid, metadata: { surface: "overlay", reason: "restart" }, }); setRecording(false); await resizeRecordingOverlay(false); try { await hideRegionSelector(); } catch { // ignore } setTimeout(() => { setStopping(false); initiateCountdown(); }, 500); } catch (err) { setError(`Restart failed: ${err}`); } }, [initiateCountdown]); const handleMarkStep = useCallback(async () => { if (!recording) return; try { await recordStepMarker("Manual Step"); // Show feedback setMarkerFeedback("Step Marked"); setTimeout(() => setMarkerFeedback(null), 2000); } catch (err) { console.error("Failed to mark step:", err); } }, [recording]); useEffect(() => { let unlisten: () => void; let unlistenMarker: () => void; const setupHotkeys = async () => { unlisten = await onToggleRecordingHotkey(() => { if (recording) { handleStopRecording(); } }); unlistenMarker = await onMarkerHotkey(() => { handleMarkStep(); }); }; setupHotkeys(); return () => { if (unlisten) unlisten(); if (unlistenMarker) unlistenMarker(); }; }, [recording, handleStopRecording, handleMarkStep]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; }; const modeOptions: { mode: RecordingMode; icon: JSX.Element; label: string }[] = [ { mode: "display", icon: icons.display, label: "Display" }, { mode: "window", icon: icons.window, label: "Window" }, { mode: "region", icon: icons.region, label: "Region" }, { mode: "terminal", icon: icons.terminal, label: "Terminal" }, ]; // Countdown view if (countdown !== null) { return (
{countdown > 0 ? countdown : "REC"}
); } return (
{recording ? ( // Recording State
{formatTime(elapsedTime)}
) : ( // Pre-recording State
{/* Mode selector */}
{modeOptions.map(({ mode: m, icon, label }) => ( ))}
{/* Window picker dropdown for window mode */} {mode === "window" && (
{windowPickerOpen && (
e.stopPropagation()} style={styles.windowPanel} > {windows.length === 0 ? (
No windows found
) : (
{windows.map((win) => { const isSelected = selectedWindow?.id === win.id; const sizeLabel = `${Math.round(win.bounds.width)} x ${Math.round(win.bounds.height)}`; return ( ); })}
)}
)}
)} {/* Region info display when in region mode */} {mode === "region" && (
{selectedRegion ? (
{regionBadgeText} @ {Math.round(selectedRegion.x)}, {Math.round(selectedRegion.y)}
) : ( )}
)} {/* Record button */} {/* Close button */}
)} {markerFeedback && (
{icons.check} {markerFeedback}
)} {error &&
{error}
}
); } export default OverlayView;