feat(admin): add audio recorder UI skeleton (MediaRecorder) and embed into EditorShell
This commit is contained in:
parent
43e6d4b53c
commit
7e0bb3dc53
@ -1,5 +1,6 @@
|
|||||||
import { Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import AdminLayout from '../layout/AdminLayout';
|
import AdminLayout from '../layout/AdminLayout';
|
||||||
|
import Recorder from '../features/recorder/Recorder';
|
||||||
|
|
||||||
export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
||||||
return (
|
return (
|
||||||
@ -7,9 +8,12 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
|||||||
<Typography variant="h4" sx={{ mb: 2 }}>
|
<Typography variant="h4" sx={{ mb: 2 }}>
|
||||||
Welcome to VoxBlog Editor
|
Welcome to VoxBlog Editor
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1">
|
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||||
Coming next: audio recorder, rich editor, AI tools, and Ghost publish flow.
|
<Recorder />
|
||||||
</Typography>
|
<Typography variant="body1">
|
||||||
|
Coming next: rich editor, AI tools, and Ghost publish flow.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
76
apps/admin/src/features/recorder/Recorder.tsx
Normal file
76
apps/admin/src/features/recorder/Recorder.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
export default function Recorder() {
|
||||||
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
|
const chunksRef = useRef<Blob[]>([]);
|
||||||
|
const [recording, setRecording] = useState(false);
|
||||||
|
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
const requestStream = async (): Promise<MediaStream | null> => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
return stream;
|
||||||
|
} catch (e) {
|
||||||
|
setError('Microphone permission denied');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
setError('');
|
||||||
|
const stream = await requestStream();
|
||||||
|
if (!stream) return;
|
||||||
|
|
||||||
|
const mr = new MediaRecorder(stream);
|
||||||
|
mediaRecorderRef.current = mr;
|
||||||
|
chunksRef.current = [];
|
||||||
|
|
||||||
|
mr.ondataavailable = (e: BlobEvent) => {
|
||||||
|
if (e.data && e.data.size > 0) {
|
||||||
|
chunksRef.current.push(e.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mr.onstop = () => {
|
||||||
|
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
setAudioUrl((prev) => {
|
||||||
|
if (prev) URL.revokeObjectURL(prev);
|
||||||
|
return url;
|
||||||
|
});
|
||||||
|
// stop all tracks to release mic
|
||||||
|
stream.getTracks().forEach(t => t.stop());
|
||||||
|
};
|
||||||
|
|
||||||
|
mr.start();
|
||||||
|
setRecording(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
mediaRecorderRef.current?.stop();
|
||||||
|
setRecording(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (audioUrl) URL.revokeObjectURL(audioUrl);
|
||||||
|
};
|
||||||
|
}, [audioUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1 }}>Audio Recorder</Typography>
|
||||||
|
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
|
||||||
|
<Button variant="contained" disabled={recording} onClick={startRecording}>Start</Button>
|
||||||
|
<Button variant="outlined" disabled={!recording} onClick={stopRecording}>Stop</Button>
|
||||||
|
</Stack>
|
||||||
|
{error && <Typography color="error" sx={{ mb: 2 }}>{error}</Typography>}
|
||||||
|
{audioUrl && (
|
||||||
|
<Box>
|
||||||
|
<audio controls src={audioUrl} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user