metadata fixes, redux start. Lifecycle fixes for regions, etc
This commit is contained in:
@ -5,6 +5,10 @@ import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import { useWavesurfer } from '@wavesurfer/react';
|
||||
import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
|
||||
@ -14,26 +18,53 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { ClipMetadata } from '../../ipc/audio/types';
|
||||
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||
import { ClipMetadata } from '../../redux/types';
|
||||
import { useAppSelector } from '../hooks';
|
||||
|
||||
export interface AudioTrimmerProps {
|
||||
metadata: ClipMetadata;
|
||||
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
|
||||
filename: string;
|
||||
onSave?: (metadata: ClipMetadata) => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export default function AudioTrimmer({
|
||||
metadata,
|
||||
filename,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: AudioTrimmerProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: metadata.filePath });
|
||||
useSortable({ id: filename });
|
||||
|
||||
const metadata = useAppSelector((state) => {
|
||||
const clip = Object.values(state.collections)
|
||||
.flat()
|
||||
.find((c) => c.filename === filename);
|
||||
return clip ?? ({ filename, name: 'Unknown Clip' } as ClipMetadata);
|
||||
});
|
||||
// Dialog state for editing name
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [nameInput, setNameInput] = useState<string>(metadata.name);
|
||||
|
||||
useEffect(() => {
|
||||
setNameInput(metadata.name);
|
||||
}, [metadata.name]);
|
||||
|
||||
const openEditDialog = () => setEditDialogOpen(true);
|
||||
const closeEditDialog = () => setEditDialogOpen(false);
|
||||
|
||||
const handleDialogSave = () => {
|
||||
if (nameInput.trim() && nameInput !== metadata.name) {
|
||||
const updated = { ...metadata, name: nameInput.trim() };
|
||||
if (onSave) onSave(updated);
|
||||
}
|
||||
closeEditDialog();
|
||||
};
|
||||
|
||||
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
||||
const containerRef = useRef(null);
|
||||
const [clipStart, setClipStart] = useState<number>(0);
|
||||
const [clipEnd, setClipEnd] = useState<number>(0);
|
||||
// const [clipStart, setClipStart] = useState<number | undefined>(undefined);
|
||||
// const [clipEnd, setClipEnd] = useState<number | undefined>(undefined);
|
||||
|
||||
const plugins = useMemo(
|
||||
() => [
|
||||
@ -46,7 +77,7 @@ export default function AudioTrimmer({
|
||||
);
|
||||
|
||||
const fileBaseName =
|
||||
metadata.filePath.split('\\').pop()?.split('/').pop() || 'Unknown';
|
||||
metadata.filename.split('\\').pop()?.split('/').pop() || 'Unknown';
|
||||
|
||||
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
||||
container: containerRef,
|
||||
@ -58,56 +89,123 @@ export default function AudioTrimmer({
|
||||
plugins,
|
||||
});
|
||||
|
||||
// Add this ref to always have the latest metadata
|
||||
const metadataRef = useRef(metadata);
|
||||
useEffect(() => {
|
||||
metadataRef.current = metadata;
|
||||
}, [metadata]);
|
||||
|
||||
const onRegionCreated = useCallback(
|
||||
(newRegion: any) => {
|
||||
if (wavesurfer === null) return;
|
||||
const allRegions = plugins[0].getRegions();
|
||||
|
||||
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||
let isNew = metadataRef.current.startTime === undefined;
|
||||
|
||||
allRegions.forEach((region) => {
|
||||
if (region.id !== newRegion.id) {
|
||||
if (
|
||||
region.start === newRegion.start &&
|
||||
region.end === newRegion.end
|
||||
) {
|
||||
newRegion.remove();
|
||||
return;
|
||||
}
|
||||
region.remove();
|
||||
isNew = !(region.start === 0 && region.end === 0);
|
||||
// console.log('Region replace:', newRegion, region);
|
||||
}
|
||||
});
|
||||
setClipStart(newRegion.start);
|
||||
setClipEnd(newRegion.end);
|
||||
|
||||
if (isNew) {
|
||||
console.log('Region created:', metadataRef.current);
|
||||
const updated = {
|
||||
...metadataRef.current,
|
||||
startTime: newRegion.start,
|
||||
endTime: newRegion.end,
|
||||
};
|
||||
if (onSave) {
|
||||
onSave(updated);
|
||||
}
|
||||
}
|
||||
},
|
||||
[plugins, wavesurfer],
|
||||
[plugins, wavesurfer, onSave],
|
||||
);
|
||||
|
||||
const onRegionUpdated = useCallback(
|
||||
(newRegion: any) => {
|
||||
if (wavesurfer === null) return;
|
||||
|
||||
const updated = {
|
||||
...metadataRef.current,
|
||||
startTime: newRegion.start,
|
||||
endTime: newRegion.end,
|
||||
};
|
||||
if (onSave) {
|
||||
onSave(updated);
|
||||
}
|
||||
},
|
||||
[onSave, wavesurfer],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('ready, setting up regions plugin', wavesurfer);
|
||||
if (metadata.startTime !== undefined && metadata.endTime !== undefined) {
|
||||
setClipStart(metadata.startTime);
|
||||
setClipEnd(metadata.endTime);
|
||||
plugins[0].addRegion({
|
||||
start: metadata.startTime,
|
||||
end: metadata.endTime,
|
||||
color: 'rgba(132, 81, 224, 0.3)',
|
||||
drag: false,
|
||||
resize: true,
|
||||
});
|
||||
} else {
|
||||
setClipStart(0);
|
||||
setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
||||
}
|
||||
const plugin = plugins[0] as RegionsPlugin;
|
||||
|
||||
plugins[0].enableDragSelection({
|
||||
if (!isReady) return;
|
||||
// console.log('ready, setting up regions plugin', plugin, isReady);
|
||||
if (
|
||||
metadataRef.current.startTime !== undefined &&
|
||||
metadataRef.current.endTime !== undefined
|
||||
) {
|
||||
// setClipStart(metadata.startTime);
|
||||
// setClipEnd(metadata.endTime);
|
||||
// console.log('Adding region from metadata:', metadata);=
|
||||
|
||||
const allRegions = plugin.getRegions();
|
||||
// console.log('Existing regions:', allRegions);
|
||||
if (
|
||||
allRegions.length === 0 ||
|
||||
(allRegions.length === 1 &&
|
||||
allRegions[0].start === 0 &&
|
||||
allRegions[0].end === 0)
|
||||
) {
|
||||
// console.log('adding region from metadata:', metadataRef.current);
|
||||
plugin.addRegion({
|
||||
start: metadataRef.current.startTime,
|
||||
end: metadataRef.current.endTime,
|
||||
color: 'rgba(132, 81, 224, 0.3)',
|
||||
drag: false,
|
||||
resize: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// setClipStart(0);
|
||||
// setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
||||
}
|
||||
}, [isReady, plugins]);
|
||||
|
||||
useEffect(() => {
|
||||
const plugin = plugins[0] as RegionsPlugin;
|
||||
plugin.unAll();
|
||||
plugin.on('region-created', onRegionCreated);
|
||||
plugin.on('region-updated', onRegionUpdated);
|
||||
plugin.enableDragSelection({
|
||||
color: 'rgba(132, 81, 224, 0.3)',
|
||||
});
|
||||
plugins[0].on('region-created', onRegionCreated);
|
||||
}, [isReady, plugins, wavesurfer, onRegionCreated, metadata]);
|
||||
}, [onRegionCreated, onRegionUpdated, plugins]);
|
||||
|
||||
useEffect(() => {
|
||||
let url: string | null = null;
|
||||
async function fetchAudio() {
|
||||
// console.log('Loading audio buffer for file:', filePath);
|
||||
const buffer = await window.audio.loadAudioBuffer(metadata.filePath);
|
||||
console.log('Received buffer:', buffer.buffer);
|
||||
// console.log('Loading audio buffer for file:', filename);
|
||||
const buffer = await window.audio.loadAudioBuffer(metadata.filename);
|
||||
// console.log('Received buffer:', buffer.buffer);
|
||||
if (buffer.buffer && !buffer.error) {
|
||||
const audioData = buffer.buffer
|
||||
? new Uint8Array(buffer.buffer)
|
||||
: buffer;
|
||||
url = URL.createObjectURL(new Blob([audioData]));
|
||||
console.log('Created blob URL:', url);
|
||||
// console.log('Created blob URL:', url);
|
||||
setBlobUrl(url);
|
||||
}
|
||||
}
|
||||
@ -115,14 +213,14 @@ export default function AudioTrimmer({
|
||||
return () => {
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
};
|
||||
}, [metadata.filePath]);
|
||||
}, [metadata.filename]);
|
||||
|
||||
const onPlayPause = () => {
|
||||
if (wavesurfer === null) return;
|
||||
if (isPlaying) {
|
||||
wavesurfer.pause();
|
||||
} else {
|
||||
const allRegions = plugins[0].getRegions();
|
||||
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||
if (allRegions.length > 0) {
|
||||
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
||||
} else {
|
||||
@ -149,7 +247,9 @@ export default function AudioTrimmer({
|
||||
className="shadow-[0_2px_8px_rgba(0,0,0,0.5)] m-2 rounded-lg bg-darkDrop"
|
||||
>
|
||||
<div
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...attributes}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...listeners}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@ -166,9 +266,63 @@ export default function AudioTrimmer({
|
||||
<div className="ml-4 mr-2 p-2">
|
||||
<div className="grid justify-items-stretch grid-cols-2">
|
||||
<div className="mb-5px flex flex-col">
|
||||
<text className="font-bold text-lg">{metadata.name}</text>
|
||||
<span
|
||||
className="font-bold text-lg text-white mb-1 cursor-pointer"
|
||||
onClick={openEditDialog}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openEditDialog();
|
||||
}
|
||||
}}
|
||||
title="Click to edit name"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
{metadata.name}
|
||||
</span>
|
||||
<text className="text-sm text-neutral-500">{fileBaseName}</text>
|
||||
</div>
|
||||
<Dialog
|
||||
open={editDialogOpen}
|
||||
onClose={closeEditDialog}
|
||||
slotProps={{
|
||||
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Edit Clip Name</DialogTitle>
|
||||
<DialogContent>
|
||||
<input
|
||||
autoFocus
|
||||
className="font-bold text-lg bg-transparent outline-none border-b border-plum focus:border-plumDark text-white mb-1 w-full text-center"
|
||||
type="text"
|
||||
value={nameInput}
|
||||
onChange={(e) => setNameInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleDialogSave();
|
||||
}}
|
||||
aria-label="Edit clip name"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeEditDialog}
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDialogSave}
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@ -198,7 +352,8 @@ export default function AudioTrimmer({
|
||||
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
||||
<div className="m-1 flex justify-start">
|
||||
<text className="text-sm ">
|
||||
Clip: {formatTime(clipStart)} - {formatTime(clipEnd)}
|
||||
Clip: {formatTime(metadata.startTime ?? 0)} -{' '}
|
||||
{formatTime(metadata.endTime ?? 0)}
|
||||
</text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user