code cleanup
This commit is contained in:
+20
-26
@@ -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<string>('');
|
||||
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 <LaunchWindow />;
|
||||
switch (windowType) {
|
||||
case 'hud-overlay':
|
||||
return <LaunchWindow />;
|
||||
case 'source-selector':
|
||||
return <SourceSelector />;
|
||||
case 'editor':
|
||||
return <VideoEditor />;
|
||||
default:
|
||||
return (
|
||||
<div className="w-full h-full bg-background text-foreground">
|
||||
<h1>Pangolin</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (windowType === 'source-selector') {
|
||||
return <SourceSelector />;
|
||||
}
|
||||
|
||||
if (windowType === 'editor') {
|
||||
return <VideoEditor />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-background text-foreground">
|
||||
<h1>Pangolin</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<DesktopSource[]>([]);
|
||||
const [selectedSource, setSelectedSource] = useState<DesktopSource | null>(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 (
|
||||
<div className="h-full bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-600 mx-auto mb-3"></div>
|
||||
<p className="text-sm text-gray-600">Loading sources...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col">
|
||||
<div className="p-4 bg-white">
|
||||
<Tabs defaultValue="screens" className="flex flex-col">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4 bg-gray-100">
|
||||
<TabsTrigger value="screens" className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-700">
|
||||
Screens
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="windows" className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-700">
|
||||
Windows
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="h-64 overflow-hidden bg-white">
|
||||
<TabsContent value="screens" className="h-full mt-0">
|
||||
<div className="grid grid-cols-2 gap-3 h-full overflow-y-auto pr-2">
|
||||
{screenSources.map((source) => (
|
||||
<Card
|
||||
key={source.id}
|
||||
className={`cursor-pointer transition-all hover:shadow-lg h-fit scale-90 p-2 ${
|
||||
selectedSource?.id === source.id
|
||||
? 'ring-2 ring-gray-700 bg-gray-50'
|
||||
: 'hover:ring-1 hover:ring-gray-300 bg-white border border-gray-200'
|
||||
}`}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="relative mb-2">
|
||||
<img
|
||||
src={source.thumbnail || ''}
|
||||
alt={source.name}
|
||||
className="w-full aspect-video object-cover rounded border border-gray-300"
|
||||
/>
|
||||
{selectedSource?.id === source.id && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<div className="w-5 h-5 bg-gray-700 rounded-full flex items-center justify-center shadow-md">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-800 truncate">
|
||||
{source.name}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="windows" className="h-full mt-0">
|
||||
<div className="grid grid-cols-2 gap-3 h-full overflow-y-auto pr-2">
|
||||
{windowSources.map((source) => (
|
||||
<Card
|
||||
key={source.id}
|
||||
className={`cursor-pointer transition-all hover:shadow-lg h-fit ${
|
||||
selectedSource?.id === source.id
|
||||
? 'ring-2 ring-gray-700 bg-gray-50'
|
||||
: 'hover:ring-1 hover:ring-gray-300 bg-white border border-gray-200'
|
||||
}`}
|
||||
style={{ transform: 'scale(0.9)', margin: '8px' }}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="relative mb-2">
|
||||
<img
|
||||
src={source.thumbnail || ''}
|
||||
alt={source.name}
|
||||
className="w-full aspect-video object-cover rounded border border-gray-300"
|
||||
/>
|
||||
{selectedSource?.id === source.id && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<div className="w-5 h-5 bg-gray-700 rounded-full flex items-center justify-center shadow-md">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{source.appIcon && (
|
||||
<img
|
||||
src={source.appIcon}
|
||||
alt="App icon"
|
||||
className="w-3 h-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="text-xs font-medium text-gray-800 truncate">
|
||||
{source.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border-t border-gray-200 p-3">
|
||||
<div className="flex justify-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.close()}
|
||||
className="px-6 py-1.5 text-sm bg-gray-600 border-gray-600 text-white hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
disabled={!selectedSource}
|
||||
className="px-6 py-1.5 text-sm bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-50 disabled:bg-gray-400"
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function VideoEditor() {
|
||||
const [videoPath, setVideoPath] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(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<HTMLInputElement>) {
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-foreground">Loading video...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-destructive">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background p-8 gap-8">
|
||||
<div className="flex flex-col flex-[7] min-w-0 gap-8">
|
||||
<div className="flex flex-col gap-6 flex-1">
|
||||
<div
|
||||
className="w-full aspect-video rounded-xl p-8 flex items-center justify-center overflow-hidden bg-cover bg-center"
|
||||
style={{ backgroundImage: 'url(/wallpaper.png)' }}
|
||||
>
|
||||
{videoPath && (
|
||||
<>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoPath}
|
||||
className="hidden"
|
||||
preload="metadata"
|
||||
onLoadedMetadata={e => {
|
||||
const { duration } = e.currentTarget;
|
||||
if (isFinite(duration) && duration > 0) setDuration(duration);
|
||||
}}
|
||||
onDurationChange={e => {
|
||||
const { duration } = e.currentTarget;
|
||||
if (isFinite(duration) && duration > 0) setDuration(duration);
|
||||
}}
|
||||
onTimeUpdate={e => {
|
||||
const time = e.currentTarget.currentTime;
|
||||
if (isFinite(time) && !isSeeking.current) setCurrentTime(time);
|
||||
}}
|
||||
onError={() => setError('Failed to load video')}
|
||||
onPlay={() => setIsPlaying(true)}
|
||||
onPause={() => setIsPlaying(false)}
|
||||
onEnded={() => setIsPlaying(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 px-4">
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-md"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="6" y="4" width="4" height="16" rx="1" />
|
||||
<rect x="14" y="4" width="4" height="16" rx="1" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<span className="text-xs text-muted-foreground font-mono tabular-nums min-w-[50px]">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 100}
|
||||
value={currentTime}
|
||||
onChange={handleSeek}
|
||||
onMouseDown={handleSeekStart}
|
||||
onMouseUp={handleSeekEnd}
|
||||
onTouchStart={handleSeekStart}
|
||||
onTouchEnd={handleSeekEnd}
|
||||
step="0.01"
|
||||
className="flex-1 h-1.5 bg-gray-200 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-transform [&::-webkit-slider-thumb]:shadow-sm hover:[&::-webkit-slider-thumb]:scale-125"
|
||||
style={{
|
||||
background: `linear-gradient(to right, rgb(59 130 246) 0%, rgb(59 130 246) ${(currentTime / (duration || 100)) * 100}%, rgb(229 231 235) ${(currentTime / (duration || 100)) * 100}%, rgb(229 231 235) 100%)`
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground font-mono tabular-nums min-w-[50px] text-right">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-border rounded-xl p-8 flex-1 min-h-[180px] flex flex-col justify-center shadow-sm">
|
||||
<div className="h-12 bg-muted rounded-lg flex items-center justify-center text-muted-foreground text-sm">
|
||||
Timeline/Editor controls coming soon...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-[3] min-w-0 bg-card border border-border rounded-xl p-8 flex flex-col shadow-sm">
|
||||
<div className="w-full h-10 bg-muted rounded-lg mb-6" />
|
||||
<div className="flex-1 w-full flex items-center justify-center text-muted-foreground text-base">
|
||||
Settings panel (coming soon)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="w-full h-full flex items-center justify-center bg-transparent">
|
||||
<div className="flex items-center gap-6 backdrop-blur-xl bg-black/80 rounded-full px-6 py-3 shadow-2xl border border-white/20">
|
||||
<div className="w-full h-full flex items-center bg-transparent">
|
||||
<div className="w-full max-w-2xl mx-auto flex items-center justify-between backdrop-blur-xl bg-black/80 rounded-full px-6 py-3 shadow-2xl border border-white/20">
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="gap-2 text-white bg-transparent hover:bg-transparent px-0"
|
||||
className="gap-2 text-white bg-transparent hover:bg-transparent px-0 flex-1 text-left"
|
||||
onClick={openSourceSelector}
|
||||
>
|
||||
<MdMonitor size={16} className="text-white" />
|
||||
@@ -61,7 +61,7 @@ export function LaunchWindow() {
|
||||
size="sm"
|
||||
onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
|
||||
disabled={!hasSelectedSource && !recording}
|
||||
className="gap-2 bg-transparent hover:bg-transparent px-0"
|
||||
className="gap-2 bg-transparent hover:bg-transparent px-0 flex-1 text-right"
|
||||
>
|
||||
{recording ? (
|
||||
<>
|
||||
@@ -78,4 +78,4 @@ export function LaunchWindow() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<DesktopSource[]>([]);
|
||||
const [selectedSource, setSelectedSource] = useState<DesktopSource | null>(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 (
|
||||
<div className="h-full flex items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-600 mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-600">Loading sources...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white">
|
||||
<div className="flex-1 flex flex-col p-4 bg-white">
|
||||
<Tabs defaultValue="screens">
|
||||
<TabsList className="grid grid-cols-2 mb-4 bg-blue-50 rounded-full">
|
||||
<TabsTrigger value="screens" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-blue-700 rounded-full">Screens</TabsTrigger>
|
||||
<TabsTrigger value="windows" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-blue-700 rounded-full">Windows</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="h-64">
|
||||
<TabsContent value="screens" className="h-full">
|
||||
<div className="grid grid-cols-2 gap-3 h-full overflow-y-auto pr-2">
|
||||
{screenSources.map(source => (
|
||||
<Card
|
||||
key={source.id}
|
||||
className={`cursor-pointer transition hover:shadow-lg h-fit p-3 scale-90 ${selectedSource?.id === source.id ? 'ring-2 ring-blue-600 bg-blue-50 z-10' : 'hover:ring-1 hover:ring-blue-200 bg-white border border-blue-100'}`}
|
||||
style={{ margin: 12, width: '80%', maxWidth: 320 }}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<div className="p-2">
|
||||
<div className="relative mb-2">
|
||||
<img
|
||||
src={source.thumbnail || ''}
|
||||
alt={source.name}
|
||||
className="w-full aspect-video object-cover rounded border border-gray-300"
|
||||
/>
|
||||
{selectedSource?.id === source.id && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center shadow-md">
|
||||
<MdCheck className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-blue-700 truncate">{source.name}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="windows" className="h-full">
|
||||
<div className="grid grid-cols-2 gap-3 h-full overflow-y-auto pr-2">
|
||||
{windowSources.map(source => (
|
||||
<Card
|
||||
key={source.id}
|
||||
className={`cursor-pointer transition hover:shadow-lg h-fit p-3 scale-90 ${selectedSource?.id === source.id ? 'ring-2 ring-blue-600 bg-blue-50 z-10' : 'hover:ring-1 hover:ring-blue-200 bg-white border border-blue-100'}`}
|
||||
style={{ margin: 12, width: '80%', maxWidth: 320 }}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<div className="p-2">
|
||||
<div className="relative mb-2">
|
||||
<img
|
||||
src={source.thumbnail || ''}
|
||||
alt={source.name}
|
||||
className="w-full aspect-video object-cover rounded border border-gray-300"
|
||||
/>
|
||||
{selectedSource?.id === source.id && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center shadow-md">
|
||||
<MdCheck className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{source.appIcon && (
|
||||
<img
|
||||
src={source.appIcon}
|
||||
alt="App icon"
|
||||
className="w-3 h-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="text-xs font-medium text-blue-700 truncate">{source.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="border-t border-blue-100 p-3">
|
||||
<div className="flex justify-center gap-3">
|
||||
<Button variant="outline" onClick={() => window.close()} className="px-6 py-1.5 text-sm bg-blue-600 border-blue-600 text-white hover:bg-blue-700">Cancel</Button>
|
||||
<Button onClick={handleShare} disabled={!selectedSource} className="px-6 py-1.5 text-sm bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:bg-blue-300">Share</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLInputElement>) {
|
||||
onSeek(parseFloat(e.target.value));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 px-4">
|
||||
<Button
|
||||
onClick={onTogglePlayPause}
|
||||
size="icon"
|
||||
className="w-8 h-8 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-md"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<MdPause width={18} height={18} />
|
||||
) : (
|
||||
<MdPlayArrow width={18} height={18} />
|
||||
)}
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration}
|
||||
value={currentTime}
|
||||
onChange={handleSeekChange}
|
||||
onMouseDown={onSeekStart}
|
||||
onMouseUp={onSeekEnd}
|
||||
onTouchStart={onSeekStart}
|
||||
onTouchEnd={onSeekEnd}
|
||||
step="0.01"
|
||||
className="flex-1 h-2 accent-blue-500 rounded-full"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${(currentTime / duration) * 100}%, #e5e7eb ${(currentTime / duration) * 100}%, #e5e7eb 100%)`
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function SettingsPanel() {
|
||||
return (
|
||||
<div className="flex-[3] min-w-0 bg-card border border-border rounded-xl p-8 flex flex-col shadow-sm">
|
||||
<div className="flex-1 w-full flex items-center justify-center text-muted-foreground text-base">
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function TimelineEditor() {
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-xl p-8 flex-1 min-h-[180px] flex flex-col justify-center shadow-sm">
|
||||
<div className="h-12 rounded-lg flex items-center justify-center text-muted-foreground text-sm">
|
||||
Timeline
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
|
||||
const videoPlaybackRef = useRef<VideoPlaybackRef>(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 (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-foreground">Loading video...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-destructive">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background p-8 gap-8">
|
||||
<div className="flex flex-col flex-[7] min-w-0 gap-8">
|
||||
<div className="flex flex-col gap-6 flex-1">
|
||||
{videoPath && (
|
||||
<>
|
||||
<VideoPlayback
|
||||
ref={videoPlaybackRef}
|
||||
videoPath={videoPath}
|
||||
isSeeking={isSeeking}
|
||||
onDurationChange={setDuration}
|
||||
onTimeUpdate={setCurrentTime}
|
||||
onPlayStateChange={setIsPlaying}
|
||||
onError={setError}
|
||||
/>
|
||||
<PlaybackControls
|
||||
isPlaying={isPlaying}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
onTogglePlayPause={togglePlayPause}
|
||||
onSeek={handleSeek}
|
||||
onSeekStart={handleSeekStart}
|
||||
onSeekEnd={handleSeekEnd}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<TimelineEditor />
|
||||
</div>
|
||||
<SettingsPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
|
||||
|
||||
interface VideoPlaybackProps {
|
||||
videoPath: string;
|
||||
isSeeking: React.MutableRefObject<boolean>;
|
||||
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<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
videoPath,
|
||||
isSeeking,
|
||||
onDurationChange,
|
||||
onTimeUpdate,
|
||||
onPlayStateChange,
|
||||
onError,
|
||||
}, ref) => {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(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 (
|
||||
<div
|
||||
className="w-full aspect-video rounded-xl p-8 flex items-center justify-center overflow-hidden bg-cover bg-center"
|
||||
style={{ backgroundImage: 'url(/wallpaper.png)' }}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoPath}
|
||||
className="hidden"
|
||||
preload="metadata"
|
||||
onLoadedMetadata={e => {
|
||||
onDurationChange(e.currentTarget.duration);
|
||||
}}
|
||||
onDurationChange={e => {
|
||||
onDurationChange(e.currentTarget.duration);
|
||||
}}
|
||||
onTimeUpdate={e => {
|
||||
if (!isSeeking.current) onTimeUpdate(e.currentTarget.currentTime);
|
||||
}}
|
||||
onError={() => onError('Failed to load video')}
|
||||
onPlay={() => onPlayStateChange(true)}
|
||||
onPause={() => onPlayStateChange(false)}
|
||||
onEnded={() => onPlayStateChange(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
VideoPlayback.displayName = 'VideoPlayback';
|
||||
|
||||
export default VideoPlayback;
|
||||
@@ -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';
|
||||
@@ -8,19 +8,19 @@ type UseScreenRecorderReturn = {
|
||||
|
||||
export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const [recording, setRecording] = useState(false);
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const chunksRef = useRef<Blob[]>([]);
|
||||
const startTimeRef = useRef<number>(0);
|
||||
const mediaRecorder = useRef<MediaRecorder | null>(null);
|
||||
const stream = useRef<MediaStream | null>(null);
|
||||
const chunks = useRef<Blob[]>([]);
|
||||
const startTime = useRef<number>(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 };
|
||||
|
||||
Reference in New Issue
Block a user