From ec37cd7f113c1bd3008e5e397144ef3fa8b28254 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Fri, 17 Oct 2025 17:06:03 -0700 Subject: [PATCH] code cleanup --- src/App.tsx | 46 ++-- src/components/SourceSelector.tsx | 203 ------------------ src/components/VideoEditor.tsx | 202 ----------------- src/components/{ => launch}/LaunchWindow.tsx | 14 +- src/components/launch/SourceSelector.tsx | 158 ++++++++++++++ .../video-editor/PlaybackControls.tsx | 72 +++++++ src/components/video-editor/SettingsPanel.tsx | 9 + .../video-editor/TimelineEditor.tsx | 9 + src/components/video-editor/VideoEditor.tsx | 106 +++++++++ src/components/video-editor/VideoPlayback.tsx | 106 +++++++++ src/components/video-editor/index.ts | 5 + src/hooks/useScreenRecorder.ts | 114 ++++------ 12 files changed, 530 insertions(+), 514 deletions(-) delete mode 100644 src/components/SourceSelector.tsx delete mode 100644 src/components/VideoEditor.tsx rename src/components/{ => launch}/LaunchWindow.tsx (85%) create mode 100644 src/components/launch/SourceSelector.tsx create mode 100644 src/components/video-editor/PlaybackControls.tsx create mode 100644 src/components/video-editor/SettingsPanel.tsx create mode 100644 src/components/video-editor/TimelineEditor.tsx create mode 100644 src/components/video-editor/VideoEditor.tsx create mode 100644 src/components/video-editor/VideoPlayback.tsx create mode 100644 src/components/video-editor/index.ts diff --git a/src/App.tsx b/src/App.tsx index 8ba9b1c..9102413 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,40 +1,34 @@ -import { LaunchWindow } from "./components/LaunchWindow"; -import { SourceSelector } from "./components/SourceSelector"; -import VideoEditor from "./components/VideoEditor"; import { useEffect, useState } from "react"; +import { LaunchWindow } from "./components/launch/LaunchWindow"; +import { SourceSelector } from "./components/launch/SourceSelector"; +import VideoEditor from "./components/video-editor/VideoEditor"; export default function App() { - const [windowType, setWindowType] = useState(''); + const [windowType, setWindowType] = useState(''); useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const type = urlParams.get('windowType') || 'default'; + const params = new URLSearchParams(window.location.search); + const type = params.get('windowType') || ''; setWindowType(type); - - // Apply transparency only for HUD overlay windows if (type === 'hud-overlay') { document.body.style.background = 'transparent'; document.documentElement.style.background = 'transparent'; - const root = document.getElementById('root'); - if (root) root.style.background = 'transparent'; + document.getElementById('root')?.style.setProperty('background', 'transparent'); } }, []); - if (windowType === 'hud-overlay') { - return ; + switch (windowType) { + case 'hud-overlay': + return ; + case 'source-selector': + return ; + case 'editor': + return ; + default: + return ( +
+

Pangolin

+
+ ); } - - if (windowType === 'source-selector') { - return ; - } - - if (windowType === 'editor') { - return ; - } - - return ( -
-

Pangolin

-
- ); } diff --git a/src/components/SourceSelector.tsx b/src/components/SourceSelector.tsx deleted file mode 100644 index 037fe2f..0000000 --- a/src/components/SourceSelector.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Card } from "@/components/ui/card"; - -interface DesktopSource { - id: string; - name: string; - thumbnail: string | null; - display_id: string; - appIcon: string | null; -} - -export function SourceSelector() { - const [sources, setSources] = useState([]); - const [selectedSource, setSelectedSource] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - loadSources(); - }, []); - - const loadSources = async () => { - try { - setLoading(true); - const rawSources = await window.electronAPI.getSources({ - types: ['screen', 'window'], - thumbnailSize: { width: 320, height: 180 }, - fetchWindowIcons: true - }); - - const formattedSources = rawSources.map(source => { - let displayName = source.name; - - if (source.id.startsWith('window:') && source.name.includes(' — ')) { - displayName = source.name.split(' — ')[1] || source.name; - } - - return { - id: source.id, - name: displayName, - thumbnail: source.thumbnail, - display_id: source.display_id, - appIcon: source.appIcon - }; - }); - - setSources(formattedSources); - } catch (error) { - console.error('Error loading sources:', error); - } finally { - setLoading(false); - } - }; - - const screenSources = sources.filter(source => source.id.startsWith('screen:')); - const windowSources = sources.filter(source => source.id.startsWith('window:')); - - const handleSourceSelect = (source: DesktopSource) => { - setSelectedSource(source); - }; - - const handleShare = async () => { - if (selectedSource) { - await window.electronAPI.selectSource(selectedSource); - } - }; - - if (loading) { - return ( -
-
-
-

Loading sources...

-
-
- ); - } - - return ( -
-
- - - - Screens - - - Windows - - - -
- -
- {screenSources.map((source) => ( - handleSourceSelect(source)} - > -
-
- {source.name} - {selectedSource?.id === source.id && ( -
-
- - - -
-
- )} -
-
- {source.name} -
-
-
- ))} -
-
- - -
- {windowSources.map((source) => ( - handleSourceSelect(source)} - > -
-
- {source.name} - {selectedSource?.id === source.id && ( -
-
- - - -
-
- )} -
-
- {source.appIcon && ( - App icon - )} -
- {source.name} -
-
-
-
- ))} -
-
-
-
-
- -
-
- - -
-
-
- ); -} \ No newline at end of file diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx deleted file mode 100644 index c6a887e..0000000 --- a/src/components/VideoEditor.tsx +++ /dev/null @@ -1,202 +0,0 @@ - - -import { useEffect, useRef, useState } from "react"; - -export default function VideoEditor() { - const [videoPath, setVideoPath] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [isPlaying, setIsPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - - const videoRef = useRef(null); - const canvasRef = useRef(null); - const isSeeking = useRef(false); - - useEffect(() => { - async function loadVideo() { - try { - const result = await window.electronAPI.getRecordedVideoPath(); - if (result.success && result.path) { - setVideoPath(`file://${result.path}`); - } else { - setError(result.message || 'Failed to load video'); - } - } catch (err) { - setError('Error loading video: ' + String(err)); - } finally { - setLoading(false); - } - } - loadVideo(); - }, []); - - useEffect(() => { - const video = videoRef.current; - const canvas = canvasRef.current; - if (!video || !canvas) return; - - let animationId: number; - function drawFrame() { - if (!video || !canvas || video.paused || video.ended) return; - if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - } - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - } - animationId = requestAnimationFrame(drawFrame); - } - const handlePlay = () => drawFrame(); - const handlePause = () => cancelAnimationFrame(animationId); - video.addEventListener('play', handlePlay); - video.addEventListener('pause', handlePause); - video.addEventListener('ended', handlePause); - return () => { - video.removeEventListener('play', handlePlay); - video.removeEventListener('pause', handlePause); - video.removeEventListener('ended', handlePause); - cancelAnimationFrame(animationId); - }; - }, [videoPath]); - - function togglePlayPause() { - if (!videoRef.current) return; - isPlaying ? videoRef.current.pause() : videoRef.current.play(); - } - function handleSeek(e: React.ChangeEvent) { - if (!videoRef.current) return; - const newTime = parseFloat(e.target.value); - videoRef.current.currentTime = newTime; - setCurrentTime(newTime); - } - function handleSeekStart() { - isSeeking.current = true; - } - function handleSeekEnd() { - isSeeking.current = false; - } - function formatTime(seconds: number) { - if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return '0:00'; - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; - } - - if (loading) { - return ( -
-
Loading video...
-
- ); - } - if (error) { - return ( -
-
{error}
-
- ); - } - - return ( -
-
-
-
- {videoPath && ( - <> - -
- -
- - - {formatTime(currentTime)} - - - - {formatTime(duration)} - -
-
- -
-
- Timeline/Editor controls coming soon... -
-
-
- -
-
-
- Settings panel (coming soon) -
-
-
- ); -} \ No newline at end of file diff --git a/src/components/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx similarity index 85% rename from src/components/LaunchWindow.tsx rename to src/components/launch/LaunchWindow.tsx index 7ab9ef1..6a1d99e 100644 --- a/src/components/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { useScreenRecorder } from "../hooks/useScreenRecorder"; -import { Button } from "@/components/ui/button"; +import { useScreenRecorder } from "../../hooks/useScreenRecorder"; +import { Button } from "../ui/button"; import { BsRecordCircle } from "react-icons/bs"; import { FaRegStopCircle } from "react-icons/fa"; import { MdMonitor } from "react-icons/md"; @@ -42,12 +42,12 @@ export function LaunchWindow() { }; return ( -
-
+
+
); -} \ No newline at end of file +} diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx new file mode 100644 index 0000000..2569d20 --- /dev/null +++ b/src/components/launch/SourceSelector.tsx @@ -0,0 +1,158 @@ +import { useState, useEffect } from "react"; +import { Button } from "../ui/button"; +import { MdCheck } from "react-icons/md"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +import { Card } from "../ui/card"; + +interface DesktopSource { + id: string; + name: string; + thumbnail: string | null; + display_id: string; + appIcon: string | null; +} + +export function SourceSelector() { + const [sources, setSources] = useState([]); + const [selectedSource, setSelectedSource] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchSources() { + setLoading(true); + try { + const rawSources = await window.electronAPI.getSources({ + types: ['screen', 'window'], + thumbnailSize: { width: 320, height: 180 }, + fetchWindowIcons: true + }); + setSources( + rawSources.map(source => ({ + id: source.id, + name: + source.id.startsWith('window:') && source.name.includes(' — ') + ? source.name.split(' — ')[1] || source.name + : source.name, + thumbnail: source.thumbnail, + display_id: source.display_id, + appIcon: source.appIcon + })) + ); + } catch (error) { + console.error('Error loading sources:', error); + } finally { + setLoading(false); + } + } + fetchSources(); + }, []); + + const screenSources = sources.filter(s => s.id.startsWith('screen:')); + const windowSources = sources.filter(s => s.id.startsWith('window:')); + + const handleSourceSelect = (source: DesktopSource) => setSelectedSource(source); + const handleShare = async () => { + if (selectedSource) await window.electronAPI.selectSource(selectedSource); + }; + + if (loading) { + return ( +
+
+
+

Loading sources...

+
+
+ ); + } + + return ( +
+
+ + + Screens + Windows + +
+ +
+ {screenSources.map(source => ( + handleSourceSelect(source)} + > +
+
+ {source.name} + {selectedSource?.id === source.id && ( +
+
+ +
+
+ )} +
+
{source.name}
+
+
+ ))} +
+
+ +
+ {windowSources.map(source => ( + handleSourceSelect(source)} + > +
+
+ {source.name} + {selectedSource?.id === source.id && ( +
+
+ +
+
+ )} +
+
+ {source.appIcon && ( + App icon + )} +
{source.name}
+
+
+
+ ))} +
+
+
+
+
+
+
+ + +
+
+
+ ); +} diff --git a/src/components/video-editor/PlaybackControls.tsx b/src/components/video-editor/PlaybackControls.tsx new file mode 100644 index 0000000..a48e5e0 --- /dev/null +++ b/src/components/video-editor/PlaybackControls.tsx @@ -0,0 +1,72 @@ +import { Button } from "../ui/button"; +import { MdPlayArrow, MdPause } from "react-icons/md"; + +interface PlaybackControlsProps { + isPlaying: boolean; + currentTime: number; + duration: number; + onTogglePlayPause: () => void; + onSeek: (time: number) => void; + onSeekStart: () => void; + onSeekEnd: () => void; +} + +export default function PlaybackControls({ + isPlaying, + currentTime, + duration, + onTogglePlayPause, + onSeek, + onSeekStart, + onSeekEnd, +}: PlaybackControlsProps) { + function formatTime(seconds: number) { + if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + } + + function handleSeekChange(e: React.ChangeEvent) { + onSeek(parseFloat(e.target.value)); + } + + return ( +
+ + + {formatTime(currentTime)} + + + + {formatTime(duration)} + +
+ ); +} diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx new file mode 100644 index 0000000..47f6155 --- /dev/null +++ b/src/components/video-editor/SettingsPanel.tsx @@ -0,0 +1,9 @@ +export default function SettingsPanel() { + return ( +
+
+ Settings +
+
+ ); +} diff --git a/src/components/video-editor/TimelineEditor.tsx b/src/components/video-editor/TimelineEditor.tsx new file mode 100644 index 0000000..070d3ba --- /dev/null +++ b/src/components/video-editor/TimelineEditor.tsx @@ -0,0 +1,9 @@ +export default function TimelineEditor() { + return ( +
+
+ Timeline +
+
+ ); +} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx new file mode 100644 index 0000000..e61ec8b --- /dev/null +++ b/src/components/video-editor/VideoEditor.tsx @@ -0,0 +1,106 @@ + + +import { useEffect, useRef, useState } from "react"; +import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback"; +import PlaybackControls from "./PlaybackControls"; +import TimelineEditor from "./TimelineEditor"; +import SettingsPanel from "./SettingsPanel"; + +export default function VideoEditor() { + const [videoPath, setVideoPath] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + + const videoPlaybackRef = useRef(null); + const isSeeking = useRef(false); + + useEffect(() => { + async function loadVideo() { + try { + const result = await window.electronAPI.getRecordedVideoPath(); + if (result.success && result.path) { + setVideoPath(`file://${result.path}`); + } else { + setError(result.message || 'Failed to load video'); + } + } catch (err) { + setError('Error loading video: ' + String(err)); + } finally { + setLoading(false); + } + } + loadVideo(); + }, []); + + function togglePlayPause() { + const video = videoPlaybackRef.current?.video; + if (!video) return; + isPlaying ? video.pause() : video.play(); + } + + function handleSeek(time: number) { + const video = videoPlaybackRef.current?.video; + if (!video) return; + video.currentTime = time; + setCurrentTime(time); + } + + function handleSeekStart() { + isSeeking.current = true; + } + + function handleSeekEnd() { + isSeeking.current = false; + } + + if (loading) { + return ( +
+
Loading video...
+
+ ); + } + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+
+
+ {videoPath && ( + <> + + + + )} +
+ +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx new file mode 100644 index 0000000..93d9175 --- /dev/null +++ b/src/components/video-editor/VideoPlayback.tsx @@ -0,0 +1,106 @@ +import { useEffect, useRef, useImperativeHandle, forwardRef } from "react"; + +interface VideoPlaybackProps { + videoPath: string; + isSeeking: React.MutableRefObject; + onDurationChange: (duration: number) => void; + onTimeUpdate: (time: number) => void; + onPlayStateChange: (playing: boolean) => void; + onError: (error: string) => void; +} + +export interface VideoPlaybackRef { + video: HTMLVideoElement | null; +} + +const VideoPlayback = forwardRef(({ + videoPath, + isSeeking, + onDurationChange, + onTimeUpdate, + onPlayStateChange, + onError, +}, ref) => { + const videoRef = useRef(null); + const canvasRef = useRef(null); + + useImperativeHandle(ref, () => ({ + video: videoRef.current, + })); + + useEffect(() => { + const video = videoRef.current; + const canvas = canvasRef.current; + if (!video || !canvas) return; + + let animationId: number; + function drawFrame() { + if (!video || !canvas) return; + if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + } + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + } + } + function drawFrameLoop() { + if (!video || !canvas || video.paused || video.ended) return; + drawFrame(); + animationId = requestAnimationFrame(drawFrameLoop); + } + const handlePlay = () => drawFrameLoop(); + const handlePause = () => cancelAnimationFrame(animationId); + const handleSeeked = () => { + drawFrame(); + }; + video.addEventListener('play', handlePlay); + video.addEventListener('pause', handlePause); + video.addEventListener('ended', handlePause); + video.addEventListener('seeked', handleSeeked); + return () => { + video.removeEventListener('play', handlePlay); + video.removeEventListener('pause', handlePause); + video.removeEventListener('ended', handlePause); + video.removeEventListener('seeked', handleSeeked); + cancelAnimationFrame(animationId); + }; + }, [videoPath]); + + return ( +
+ +
+ ); +}); + +VideoPlayback.displayName = 'VideoPlayback'; + +export default VideoPlayback; diff --git a/src/components/video-editor/index.ts b/src/components/video-editor/index.ts new file mode 100644 index 0000000..f7db441 --- /dev/null +++ b/src/components/video-editor/index.ts @@ -0,0 +1,5 @@ +export { default as VideoEditor } from './VideoEditor'; +export { default as VideoPlayback } from './VideoPlayback'; +export { default as PlaybackControls } from './PlaybackControls'; +export { default as TimelineEditor } from './TimelineEditor'; +export { default as SettingsPanel } from './SettingsPanel'; diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index d7a2d43..bb250da 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -8,19 +8,19 @@ type UseScreenRecorderReturn = { export function useScreenRecorder(): UseScreenRecorderReturn { const [recording, setRecording] = useState(false); - const mediaRecorderRef = useRef(null); - const streamRef = useRef(null); - const chunksRef = useRef([]); - const startTimeRef = useRef(0); + const mediaRecorder = useRef(null); + const stream = useRef(null); + const chunks = useRef([]); + const startTime = useRef(0); useEffect(() => { return () => { - if (mediaRecorderRef.current?.state === "recording") { - mediaRecorderRef.current.stop(); + if (mediaRecorder.current?.state === "recording") { + mediaRecorder.current.stop(); } - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - streamRef.current = null; + if (stream.current) { + stream.current.getTracks().forEach(track => track.stop()); + stream.current = null; } }; }, []); @@ -28,15 +28,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const startRecording = async () => { try { const selectedSource = await window.electronAPI.getSelectedSource(); - if (!selectedSource) { alert("Please select a source to record"); return; } - await window.electronAPI.startMouseTracking(); - - const stream = await (navigator.mediaDevices as any).getUserMedia({ + const mediaStream = await (navigator.mediaDevices as any).getUserMedia({ audio: false, video: { mandatory: { @@ -45,113 +42,78 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }, }, }); - streamRef.current = stream; - - if (!streamRef.current) { - throw new Error("Failed to get media stream"); + stream.current = mediaStream; + if (!stream.current) { + throw new Error("Media stream is not available."); } - - const videoTrack = streamRef.current.getVideoTracks()[0]; - const settings = videoTrack.getSettings(); - const width = settings.width || 1920; - const height = settings.height || 1080; + const videoTrack = stream.current.getVideoTracks()[0]; + const { width = 1920, height = 1080 } = videoTrack.getSettings(); const totalPixels = width * height; - - let bitrate: number; - if (totalPixels <= 1920 * 1080) { - bitrate = 150_000_000; - } else if (totalPixels <= 2560 * 1440) { + let bitrate = 150_000_000; + if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) { bitrate = 250_000_000; - } else { + } else if (totalPixels > 2560 * 1440) { bitrate = 400_000_000; } - - chunksRef.current = []; + chunks.current = []; const mimeType = "video/webm;codecs=vp9"; - const recorder = new MediaRecorder(streamRef.current, { - mimeType, - videoBitsPerSecond: bitrate, - }); - mediaRecorderRef.current = recorder; - - recorder.ondataavailable = (e) => { - if (e.data && e.data.size > 0) { - chunksRef.current.push(e.data); - } + const recorder = new MediaRecorder(stream.current, { mimeType, videoBitsPerSecond: bitrate }); + mediaRecorder.current = recorder; + recorder.ondataavailable = e => { + if (e.data && e.data.size > 0) chunks.current.push(e.data); }; - recorder.onstop = async () => { - streamRef.current = null; - - if (chunksRef.current.length === 0) return; - - const duration = Date.now() - startTimeRef.current; - const buggyBlob = new Blob(chunksRef.current, { type: mimeType }); + stream.current = null; + if (chunks.current.length === 0) return; + const duration = Date.now() - startTime.current; + const buggyBlob = new Blob(chunks.current, { type: mimeType }); const timestamp = Date.now(); const videoFileName = `recording-${timestamp}.webm`; const trackingFileName = `recording-${timestamp}_tracking.json`; - try { const videoBlob = await fixWebmDuration(buggyBlob, duration); const arrayBuffer = await videoBlob.arrayBuffer(); - - const videoResult = await window.electronAPI.storeRecordedVideo( - arrayBuffer, - videoFileName - ); - + const videoResult = await window.electronAPI.storeRecordedVideo(arrayBuffer, videoFileName); if (!videoResult.success) { console.error('Failed to store video:', videoResult.message); return; } - const trackingResult = await window.electronAPI.storeMouseTrackingData(trackingFileName); - if (!trackingResult.success) { console.warn('Failed to store mouse tracking:', trackingResult.message); } - await window.electronAPI.switchToEditor(); } catch (error) { console.error('Error saving recording:', error); } }; - - recorder.onerror = () => { - setRecording(false); - }; - + recorder.onerror = () => setRecording(false); recorder.start(1000); - startTimeRef.current = Date.now(); + startTime.current = Date.now(); setRecording(true); } catch (error) { console.error('Failed to start recording:', error); setRecording(false); - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - streamRef.current = null; + if (stream.current) { + stream.current.getTracks().forEach(track => track.stop()); + stream.current = null; } } }; const stopRecording = () => { - if (mediaRecorderRef.current?.state === "recording") { - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); + if (mediaRecorder.current?.state === "recording") { + if (stream.current) { + stream.current.getTracks().forEach(track => track.stop()); } - - mediaRecorderRef.current.stop(); + mediaRecorder.current.stop(); setRecording(false); window.electronAPI.stopMouseTracking(); } }; const toggleRecording = () => { - if (!recording) { - startRecording(); - } else { - stopRecording(); - } + recording ? stopRecording() : startRecording(); }; return { recording, toggleRecording };