- F481: Co-listening sessions (WebSocket sync, ListenTogether page) - F482: Stem sharing (upload/list/download wav,aiff,flac) - F483: Collaborative rooms (type collaborative, max 10, invite-only) - Roadmap: v0.10.7 → DONE
124 lines
4 KiB
TypeScript
124 lines
4 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Upload, Download, Loader2, Disc3 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/ui/card';
|
|
import { trackStemService, type TrackStem } from '../services/trackStemService';
|
|
import type { Track } from '../types/track';
|
|
|
|
interface TrackStemsSectionProps {
|
|
track: Track;
|
|
isCreator: boolean;
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
export function TrackStemsSection({ track, isCreator }: TrackStemsSectionProps) {
|
|
const queryClient = useQueryClient();
|
|
const { data: stems, isLoading, error } = useQuery({
|
|
queryKey: ['track-stems', track.id],
|
|
queryFn: () => trackStemService.listStems(track.id),
|
|
});
|
|
|
|
const uploadMutation = useMutation({
|
|
mutationFn: ({ file, name }: { file: File; name?: string }) =>
|
|
trackStemService.uploadStem(track.id, file, name),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['track-stems', track.id] });
|
|
},
|
|
});
|
|
|
|
const handleUpload = () => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.wav,.aiff,.aif,.flac';
|
|
input.onchange = async (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
if (!file) return;
|
|
uploadMutation.mutate({ file, name: file.name.replace(/\.[^.]+$/, '') });
|
|
};
|
|
input.click();
|
|
};
|
|
|
|
const handleDownload = async (stem: TrackStem) => {
|
|
await trackStemService.downloadStem(track.id, stem.name, stem.format);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card variant="glass" className="p-6">
|
|
<div className="flex items-center gap-3 text-muted-foreground">
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
Loading stems...
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card variant="glass" className="p-6">
|
|
<p className="text-destructive">Failed to load stems.</p>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const hasStems = stems && stems.length > 0;
|
|
const showUpload = isCreator;
|
|
|
|
return (
|
|
<Card variant="glass" className="p-6">
|
|
<div className="flex items-center justify-between gap-3 mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex justify-center w-8 h-8 rounded-lg bg-primary/10">
|
|
<Disc3 className="w-4 h-4 text-primary" />
|
|
</div>
|
|
<h3 className="text-heading-3">Stems</h3>
|
|
</div>
|
|
{showUpload && (
|
|
<Button
|
|
onClick={handleUpload}
|
|
disabled={uploadMutation.isPending}
|
|
size="sm"
|
|
variant="outline"
|
|
>
|
|
{uploadMutation.isPending ? (
|
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
) : (
|
|
<Upload className="w-4 h-4 mr-2" />
|
|
)}
|
|
Upload
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{!hasStems && !showUpload ? (
|
|
<p className="text-muted-foreground">No stems available.</p>
|
|
) : !hasStems ? (
|
|
<p className="text-muted-foreground">Upload stems (WAV, AIFF, FLAC) to share with collaborators.</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{stems!.map((stem) => (
|
|
<li
|
|
key={stem.id}
|
|
className="flex items-center justify-between gap-4 py-2 px-3 rounded-lg bg-muted/30 hover:bg-muted/50"
|
|
>
|
|
<span className="font-medium truncate">{stem.name}</span>
|
|
<span className="text-sm text-muted-foreground">{stem.format} · {formatBytes(stem.size_bytes)}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDownload(stem)}
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|