Skip to content

Commit 8d7ae32

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Implement post-processing editor with trim and delete controls
1 parent b420a9c commit 8d7ae32

4 files changed

Lines changed: 185 additions & 145 deletions

File tree

src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,8 +2527,9 @@ function App() {
25272527
importSession={recorder.importSession}
25282528
exportVideo={handleExportVideo}
25292529
updateMetadata={recorder.updateMetadata}
2530-
isAudioEnabled={recorder.isAudioEnabled}
2531-
setIsAudioEnabled={recorder.setIsAudioEnabled}
2530+
trimSession={recorder.trimSession}
2531+
deleteEvent={recorder.deleteEvent}
2532+
deleteEventsByType={recorder.deleteEventsByType}
25322533
isLightMode={isLightMode}
25332534
cardBg={isLightMode ? 'bg-white' : 'bg-neutral-900'}
25342535
/>

src/components/RecorderControls.tsx

Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useRef, useState, useEffect } from 'react';
1+
import { useRef, useState } from 'react';
22
import {
33
Play, Pause, Circle, Square,
4-
Upload, Save, Film, Pencil, Mic
4+
Upload, Save, Film, Pencil, Scissors, ChevronDown, Trash2
55
} from 'lucide-react';
66
import type { RecordedSession } from '../types';
77

@@ -23,9 +23,9 @@ interface RecorderControlsProps {
2323
importSession: (file: File) => void;
2424
exportVideo: () => void;
2525
updateMetadata: (updates: any) => void;
26-
27-
isAudioEnabled: boolean;
28-
setIsAudioEnabled: (enabled: boolean) => void;
26+
trimSession: (startTime: number, endTime: number) => void;
27+
deleteEvent: (index: number) => void;
28+
deleteEventsByType: (type: string, fromTime?: number, toTime?: number) => void;
2929

3030
isLightMode: boolean;
3131
cardBg: string;
@@ -42,51 +42,15 @@ export const RecorderControls = ({
4242
isRecording, isPlaying, recordingTime, playbackTime, session, playbackSpeed,
4343
startRecording, stopRecording, play, pause, seek, setPlaybackSpeed,
4444
exportSession, importSession, exportVideo, updateMetadata,
45-
isAudioEnabled, setIsAudioEnabled,
45+
trimSession, deleteEvent, deleteEventsByType,
4646
isLightMode, cardBg
4747
}: RecorderControlsProps) => {
4848

4949
const fileInputRef = useRef<HTMLInputElement>(null);
5050
const [isEditingTitle, setIsEditingTitle] = useState(false);
51-
const audioRef = useRef<HTMLAudioElement>(null);
52-
53-
// Audio playback sync
54-
useEffect(() => {
55-
if (!session?.metadata.audioData || !audioRef.current) return;
56-
57-
// Decode base64 and create audio blob
58-
const audioBlob = new Blob(
59-
[Uint8Array.from(atob(session.metadata.audioData), c => c.charCodeAt(0))],
60-
{ type: session.metadata.audioMimeType || 'audio/webm' }
61-
);
62-
const audioUrl = URL.createObjectURL(audioBlob);
63-
audioRef.current.src = audioUrl;
64-
65-
return () => URL.revokeObjectURL(audioUrl);
66-
}, [session]);
67-
68-
// Sync audio with playback
69-
useEffect(() => {
70-
if (!audioRef.current || !session?.metadata.audioData) return;
71-
72-
if (isPlaying) {
73-
audioRef.current.play().catch(err => console.error('Audio play failed:', err));
74-
} else {
75-
audioRef.current.pause();
76-
}
77-
}, [isPlaying, session]);
78-
79-
// Sync audio seek
80-
useEffect(() => {
81-
if (!audioRef.current || !session?.metadata.audioData) return;
82-
audioRef.current.currentTime = playbackTime / 1000; // Convert ms to seconds
83-
}, [playbackTime, session]);
84-
85-
// Sync playback speed
86-
useEffect(() => {
87-
if (!audioRef.current || !session?.metadata.audioData) return;
88-
audioRef.current.playbackRate = playbackSpeed;
89-
}, [playbackSpeed, session]);
51+
const [isEditPanelOpen, setIsEditPanelOpen] = useState(false);
52+
const [trimStart, setTrimStart] = useState('00:00');
53+
const [trimEnd, setTrimEnd] = useState('00:00');
9054

9155
// Only show full detailed controls if we have a session or are recording
9256
// Otherwise show a compact "Start Recording" or "Load Session" button
@@ -182,14 +146,6 @@ export const RecorderControls = ({
182146
{/* File Controls (Load/Save) */}
183147
{!isRecording && (
184148
<div className="flex gap-1">
185-
{/* Mic Toggle */}
186-
<button
187-
onClick={() => setIsAudioEnabled(!isAudioEnabled)}
188-
className={`p-1 transition-colors ${isAudioEnabled ? 'text-red-500' : 'hover:text-blue-500'}`}
189-
title={isAudioEnabled ? 'Microphone enabled' : 'Enable microphone'}
190-
>
191-
<Mic className="w-3.5 h-3.5" />
192-
</button>
193149
<button
194150
onClick={() => fileInputRef.current?.click()}
195151
className="p-1 hover:text-blue-500 transition-colors"
@@ -264,8 +220,98 @@ export const RecorderControls = ({
264220
</div>
265221
)}
266222

267-
{/* Hidden audio element for narration playback */}
268-
<audio ref={audioRef} style={{ display: 'none' }} />
223+
{/* Edit Panel (Only if session exists and not recording) */}
224+
{session && !isRecording && (
225+
<div className="space-y-2">
226+
<button
227+
onClick={() => setIsEditPanelOpen(!isEditPanelOpen)}
228+
className={`w-full flex items-center justify-between p-2 rounded ${styles.button} text-xs`}
229+
>
230+
<div className="flex items-center gap-2">
231+
<Scissors className="w-3.5 h-3.5" />
232+
<span className={styles.text}>Edit Recording</span>
233+
</div>
234+
<ChevronDown className={`w-3 h-3 transition-transform ${isEditPanelOpen ? 'rotate-180' : ''}`} />
235+
</button>
236+
237+
{isEditPanelOpen && (
238+
<div className="space-y-3 p-3 bg-black/20 rounded-lg">
239+
{/* Trim Controls */}
240+
<div className="space-y-2">
241+
<label className={`${styles.text} block`}>Trim</label>
242+
<div className="flex gap-2 items-center">
243+
<input
244+
type="text"
245+
value={trimStart}
246+
onChange={(e) => setTrimStart(e.target.value)}
247+
placeholder="00:00"
248+
className="bg-black/30 border border-white/10 rounded px-2 py-1 text-xs w-16 font-mono"
249+
/>
250+
<span className="text-xs opacity-50">to</span>
251+
<input
252+
type="text"
253+
value={trimEnd}
254+
onChange={(e) => setTrimEnd(e.target.value)}
255+
placeholder={formatTime(session.metadata.duration)}
256+
className="bg-black/30 border border-white/10 rounded px-2 py-1 text-xs w-16 font-mono"
257+
/>
258+
<button
259+
onClick={() => {
260+
const start = parseTimeString(trimStart);
261+
const end = parseTimeString(trimEnd) || session.metadata.duration;
262+
if (start < end) {
263+
trimSession(start, end);
264+
setIsEditPanelOpen(false);
265+
}
266+
}}
267+
className="px-3 py-1 bg-purple-600 hover:bg-purple-700 rounded text-xs transition-colors"
268+
>
269+
Apply
270+
</button>
271+
</div>
272+
</div>
273+
274+
{/* Event Deletion Controls */}
275+
<div className="space-y-2">
276+
<label className={`${styles.text} block`}>Delete Events</label>
277+
<div className="flex gap-2 flex-wrap">
278+
<button
279+
onClick={() => {
280+
const lastIdx = session.events.length - 1;
281+
if (lastIdx >= 0) deleteEvent(lastIdx);
282+
}}
283+
className="px-2 py-1 bg-red-600/20 hover:bg-red-600/40 border border-red-600/30 rounded text-xs transition-colors flex items-center gap-1"
284+
>
285+
<Trash2 className="w-3 h-3" />
286+
Undo Last
287+
</button>
288+
<button
289+
onClick={() => deleteEventsByType('camera')}
290+
className="px-2 py-1 bg-blue-600/20 hover:bg-blue-600/40 border border-blue-600/30 rounded text-xs transition-colors"
291+
>
292+
Delete Camera
293+
</button>
294+
<button
295+
onClick={() => deleteEventsByType('annotation')}
296+
className="px-2 py-1 bg-yellow-600/20 hover:bg-yellow-600/40 border border-yellow-600/30 rounded text-xs transition-colors"
297+
>
298+
Delete Annotations
299+
</button>
300+
</div>
301+
</div>
302+
</div>
303+
)}
304+
</div>
305+
)}
269306
</div>
270307
);
271308
};
309+
310+
// Helper function to parse MM:SS format
311+
const parseTimeString = (timeStr: string): number => {
312+
const parts = timeStr.split(':');
313+
if (parts.length !== 2) return 0;
314+
const minutes = parseInt(parts[0], 10) || 0;
315+
const seconds = parseInt(parts[1], 10) || 0;
316+
return (minutes * 60 + seconds) * 1000; // Convert to milliseconds
317+
};

0 commit comments

Comments
 (0)