collection list, move, delete
This commit is contained in:
@ -10,41 +10,42 @@ 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 RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import PauseIcon from '@mui/icons-material/Pause';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||
import { ClipMetadata } from '../../redux/types';
|
||||
import { useAppSelector } from '../hooks';
|
||||
|
||||
export interface AudioTrimmerProps {
|
||||
filename: string;
|
||||
metadata: ClipMetadata;
|
||||
onSave?: (metadata: ClipMetadata) => void;
|
||||
onDelete?: () => void;
|
||||
onDelete?: (metadata: ClipMetadata) => void;
|
||||
onMove?: (newCollection: string, metadata: ClipMetadata) => void;
|
||||
}
|
||||
|
||||
export default function AudioTrimmer({
|
||||
filename,
|
||||
metadata,
|
||||
onSave,
|
||||
onDelete,
|
||||
onMove,
|
||||
}: AudioTrimmerProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: filename });
|
||||
useSortable({ id: metadata.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 [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [nameInput, setNameInput] = useState<string>(metadata.name);
|
||||
const collectionNames = useAppSelector((state) =>
|
||||
state.collections.map((col) => col.name),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNameInput(metadata.name);
|
||||
@ -68,7 +69,7 @@ export default function AudioTrimmer({
|
||||
|
||||
const plugins = useMemo(
|
||||
() => [
|
||||
Regions.create(),
|
||||
RegionsPlugin.create(),
|
||||
ZoomPlugin.create({
|
||||
scale: 0.25,
|
||||
}),
|
||||
@ -282,7 +283,7 @@ export default function AudioTrimmer({
|
||||
>
|
||||
{metadata.name}
|
||||
</span>
|
||||
<text className="text-sm text-neutral-500">{fileBaseName}</text>
|
||||
<span className="text-sm text-neutral-500">{fileBaseName}</span>
|
||||
</div>
|
||||
<Dialog
|
||||
open={editDialogOpen}
|
||||
@ -294,6 +295,7 @@ export default function AudioTrimmer({
|
||||
<DialogTitle>Edit Clip Name</DialogTitle>
|
||||
<DialogContent>
|
||||
<input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
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"
|
||||
@ -302,6 +304,7 @@ export default function AudioTrimmer({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleDialogSave();
|
||||
}}
|
||||
onFocus={(event) => event.target.select()}
|
||||
aria-label="Edit clip name"
|
||||
/>
|
||||
</DialogContent>
|
||||
@ -323,6 +326,38 @@ export default function AudioTrimmer({
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={deleteDialogOpen}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
slotProps={{
|
||||
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
Are you sure you want to delete this clip?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(false);
|
||||
if (onDelete) onDelete(metadataRef.current);
|
||||
}}
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@ -331,16 +366,36 @@ export default function AudioTrimmer({
|
||||
>
|
||||
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
|
||||
</button>
|
||||
<div className="relative inline-block">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||
onClick={() => setDropdownOpen((prev) => !prev)}
|
||||
>
|
||||
{dropdownOpen ? <ArrowDownwardIcon /> : <ArrowForwardIcon />}
|
||||
</button>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute z-10 mt-2 w-40 bg-midnight rounded-md shadow-lg">
|
||||
{collectionNames.map((name) => (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className="block w-full text-left px-4 py-2 text-white hover:bg-plumDark"
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
if (onMove) onMove(name, metadata);
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||
onClick={onDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@ -21,7 +22,8 @@ export interface ClipListProps {
|
||||
|
||||
export default function ClipList({ collection }: ClipListProps) {
|
||||
const metadata = useAppSelector(
|
||||
(state) => state.collections[collection] || [],
|
||||
(state) =>
|
||||
state.collections.find((col) => col.name === collection) || { clips: [] },
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -29,14 +31,19 @@ export default function ClipList({ collection }: ClipListProps) {
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
);
|
||||
|
||||
async function handleDragEnd(event) {
|
||||
async function handleDragEnd(event: any) {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
const oldIndex = metadata.findIndex(
|
||||
const oldIndex = metadata.clips.findIndex(
|
||||
(item) => item.filename === active.id,
|
||||
);
|
||||
const newIndex = metadata.findIndex((item) => item.filename === over.id);
|
||||
const newMetadata = arrayMove(metadata, oldIndex, newIndex);
|
||||
const newIndex = metadata.clips.findIndex(
|
||||
(item) => item.filename === over.id,
|
||||
);
|
||||
const newMetadata = {
|
||||
...metadata,
|
||||
clips: arrayMove(metadata.clips, oldIndex, newIndex),
|
||||
};
|
||||
console.log('New order:', newMetadata);
|
||||
dispatch({
|
||||
type: 'metadata/setCollections',
|
||||
@ -52,7 +59,7 @@ export default function ClipList({ collection }: ClipListProps) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: collection,
|
||||
clips: newMetadata,
|
||||
clips: newMetadata.clips,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@ -66,6 +73,47 @@ export default function ClipList({ collection }: ClipListProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(meta: ClipMetadata) {
|
||||
dispatch({
|
||||
type: 'metadata/deleteClip',
|
||||
payload: { collection, clip: meta },
|
||||
});
|
||||
fetch('http://localhost:5010/meta/collection/clips/remove', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: collection,
|
||||
clip: meta,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.error('Error deleting clip:', err));
|
||||
console.log('Deleting clip:', meta);
|
||||
}
|
||||
|
||||
async function handleClipMove(targetCollection: string, meta: ClipMetadata) {
|
||||
console.log('Moving clip:', meta, 'to collection:', targetCollection);
|
||||
dispatch({
|
||||
type: 'metadata/moveClip',
|
||||
payload: { sourceCollection: collection, targetCollection, clip: meta },
|
||||
});
|
||||
fetch('http://localhost:5010/meta/collection/clips/move', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sourceCollection: collection,
|
||||
targetCollection,
|
||||
clip: meta,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.error('Error moving clip:', err));
|
||||
}
|
||||
|
||||
async function handleClipSave(meta: ClipMetadata) {
|
||||
try {
|
||||
dispatch({
|
||||
@ -85,7 +133,7 @@ export default function ClipList({ collection }: ClipListProps) {
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = await response.json();
|
||||
await response.json();
|
||||
// console.log('handle clip save return:', data.collections);
|
||||
dispatch({
|
||||
type: 'metadata/editClip',
|
||||
@ -97,24 +145,42 @@ export default function ClipList({ collection }: ClipListProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center bg-midnight text-offwhite">
|
||||
<div className="min-h-full flex flex-col justify-start bg-midnight text-offwhite">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onDragEnd={handleDragEnd}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<SortableContext
|
||||
items={metadata.map((item) => item.filename)}
|
||||
items={metadata.clips.map((item) => item.filename)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{metadata.map((trimmer) => (
|
||||
{metadata.clips.map((trimmer, idx) => (
|
||||
<React.Fragment key={trimmer.filename}>
|
||||
<AudioTrimmer
|
||||
metadata={trimmer}
|
||||
onSave={handleClipSave}
|
||||
onDelete={handleDelete}
|
||||
onMove={handleClipMove}
|
||||
/>
|
||||
{(idx + 1) % 10 === 0 && idx !== metadata.clips.length - 1 && (
|
||||
<div className="my-4 border-t border-gray-500">
|
||||
<p className="text-center text-sm text-gray-400">
|
||||
-- Page {Math.ceil((idx + 1) / 10) + 1} --
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* {metadata.map((trimmer) => (
|
||||
<AudioTrimmer
|
||||
key={trimmer.filename}
|
||||
filename={trimmer.filename}
|
||||
onSave={handleClipSave}
|
||||
/>
|
||||
))}
|
||||
))} */}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user