import { useState, useRef, useEffect, useCallback } from "react"; import { startRecording, stopRecording, onToggleRecordingHotkey, resizeRecordingOverlay, hideRecordingOverlay, } from "../lib/api"; import { trackWorkflowEvent } from "../lib/usageMetrics"; export interface UseRecordingProps { onStart?: (sessionId: string) => void; onStop?: (sessionId: string) => void; onError?: (error: string) => void; } export const useRecording = ({ onStart, onStop, onError }: UseRecordingProps = {}) => { const [recording, setRecording] = useState(false); const [stopping, setStopping] = useState(false); const [elapsedTime, setElapsedTime] = useState(0); const [sessionId, setSessionId] = useState(null); const timerRef = useRef(null); // Refs for hotkey access const recordingRef = useRef(recording); const stoppingRef = useRef(stopping); useEffect(() => { recordingRef.current = recording; stoppingRef.current = stopping; }, [recording, stopping]); // Timer logic useEffect(() => { if (recording) { timerRef.current = setInterval(() => { setElapsedTime((prev) => prev + 1); }, 1000); } else { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } setElapsedTime(0); } return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [recording]); const start = useCallback( async (displayId: string, width: number, height: number) => { try { const id = await startRecording(displayId, width, height); setSessionId(id); setRecording(true); setElapsedTime(0); trackWorkflowEvent("recording_started", { sessionId: id, metadata: { surface: "recording_view", displayId }, }); // If running in overlay mode, resize it try { // This might fail if not in overlay window, which is fine await resizeRecordingOverlay(true); } catch (_err) { // Ignore } onStart?.(id); return id; } catch (err) { const msg = String(err); onError?.(msg); throw new Error(msg); } }, [onStart, onError] ); const stop = useCallback(async () => { if (stoppingRef.current) return; // Optimistic UI update setStopping(true); setRecording(false); setElapsedTime(0); try { const id = await stopRecording(); trackWorkflowEvent("recording_stopped", { sessionId: id, metadata: { surface: "recording_view" }, }); // Hide overlay if visible try { await hideRecordingOverlay(); } catch (_err) { // Ignore } onStop?.(id); return id; } catch (err) { const msg = String(err); onError?.(msg); setStopping(false); // Revert stopping state on error throw new Error(msg); } finally { setStopping(false); } }, [onStop, onError]); const cancel = useCallback(async () => { if (stoppingRef.current) return; setStopping(true); try { const id = await stopRecording(); trackWorkflowEvent("recording_stopped", { sessionId: id, metadata: { surface: "recording_view", reason: "cancel" }, }); setRecording(false); setElapsedTime(0); try { await hideRecordingOverlay(); } catch (_err) { // Ignore } } catch (err) { onError?.(String(err)); } finally { setStopping(false); } }, [onError]); // Hotkey listener useEffect(() => { const setupHotkey = async () => { const unlisten = await onToggleRecordingHotkey(() => { if (stoppingRef.current) return; if (recordingRef.current) { stop(); } else { // Start logic is complex (needs display selection), // so we usually just show the overlay or focus the window // handled by global shortcut logic in Rust } }); return unlisten; }; let unlistenFn: (() => void) | null = null; setupHotkey().then((fn) => { unlistenFn = fn; }); return () => { unlistenFn?.(); }; }, [stop]); return { recording, stopping, elapsedTime, sessionId, start, stop, cancel, }; };