nex_music/components/UploadOverlay.tsx

301 lines
12 KiB
TypeScript

import React, { useState, ChangeEvent, useEffect } from 'react';
import { Upload, Music, FileText, Image as ImageIcon, X, Link, ListMusic } from 'lucide-react';
import { TrackData } from '../types';
import { parseLrc, fetchPlaylist } from '../utils/parsers';
interface UploadOverlayProps {
isOpen: boolean;
onClose: () => void;
onTrackLoaded: (track: TrackData) => void;
onPlaylistLoaded: (playlist: TrackData[]) => void;
currentPlaylistUrl: string;
}
const UploadOverlay: React.FC<UploadOverlayProps> = ({
isOpen,
onClose,
onTrackLoaded,
onPlaylistLoaded,
currentPlaylistUrl
}) => {
// Playlist State
const [playlistUrl, setPlaylistUrl] = useState(currentPlaylistUrl);
const [playlistLoading, setPlaylistLoading] = useState(false);
// Local Upload State
const [audioFile, setAudioFile] = useState<File | null>(null);
const [coverFile, setCoverFile] = useState<File | null>(null);
const [lrcFile, setLrcFile] = useState<File | null>(null);
const [title, setTitle] = useState('Unknown Track');
const [artist, setArtist] = useState('Unknown Artist');
const [localLoading, setLocalLoading] = useState(false);
// Tab state
const [activeTab, setActiveTab] = useState<'playlist' | 'local'>('playlist');
useEffect(() => {
setPlaylistUrl(currentPlaylistUrl);
}, [currentPlaylistUrl]);
if (!isOpen) return null;
const handleFileChange = (e: ChangeEvent<HTMLInputElement>, setter: (f: File | null) => void) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setter(file);
// Auto-parse filename if it's the audio file
if (setter === setAudioFile) {
// Remove extension
const filename = file.name.replace(/\.[^/.]+$/, "");
// Try parsing "Artist - Title" format
const parts = filename.split(' - ');
if (parts.length >= 2) {
setArtist(parts[1].trim());
setTitle(parts.slice(0).join(' - ').trim());
} else {
setArtist("Unknown Artist");
setTitle(filename);
}
}
}
};
const handleLoadPlaylist = async () => {
if (!playlistUrl) return;
setPlaylistLoading(true);
try {
const tracks = await fetchPlaylist(playlistUrl);
if (tracks.length > 0) {
onPlaylistLoaded(tracks);
onClose();
} else {
alert("No tracks found in playlist or invalid format.");
}
} catch (e) {
alert("Failed to load playlist.");
} finally {
setPlaylistLoading(false);
}
};
const handleLocalLoad = async () => {
if (!audioFile) return;
setLocalLoading(true);
try {
const audioUrl = URL.createObjectURL(audioFile);
const coverUrl = coverFile
? URL.createObjectURL(coverFile)
: 'https://picsum.photos/400/400'; // Fallback
let lyrics: any[] = [];
if (lrcFile) {
const text = await lrcFile.text();
lyrics = parseLrc(text);
}
const track: TrackData = {
title: title || audioFile.name.replace(/\.[^/.]+$/, ""),
artist: artist || "Unknown Artist",
audioUrl,
coverUrl,
lyrics
};
onTrackLoaded(track);
onClose();
} catch (error) {
console.error("Error loading track", error);
} finally {
setLocalLoading(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-neutral-900 border border-neutral-700 rounded-2xl p-6 w-full max-w-lg shadow-2xl relative max-h-[90vh] overflow-y-auto">
<button
onClick={onClose}
className="absolute top-4 right-4 text-neutral-400 hover:text-white"
>
<X size={24} />
</button>
<h2 className="text-2xl font-bold mb-6 text-white flex items-center gap-2">
<Music className="text-amber-500" /> Library & Source
</h2>
{/* Tabs */}
<div className="flex gap-4 mb-6 border-b border-neutral-700 pb-2">
<button
onClick={() => setActiveTab('playlist')}
className={`text-sm font-bold pb-2 transition-colors ${activeTab === 'playlist' ? 'text-white border-b-2 border-amber-500' : 'text-neutral-500 hover:text-neutral-300'}`}
>
Remote Playlist
</button>
<button
onClick={() => setActiveTab('local')}
className={`text-sm font-bold pb-2 transition-colors ${activeTab === 'local' ? 'text-white border-b-2 border-amber-500' : 'text-neutral-500 hover:text-neutral-300'}`}
>
Local File (Single)
</button>
</div>
{activeTab === 'playlist' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-neutral-400 mb-2">Playlist JSON URL</label>
<div className="flex gap-2">
<div className="relative flex-1">
<Link size={16} className="absolute left-3 top-3 text-neutral-500" />
<input
type="text"
value={playlistUrl}
onChange={(e) => setPlaylistUrl(e.target.value)}
className="w-full bg-neutral-800 border border-neutral-700 rounded-lg pl-9 pr-3 py-2.5 text-sm text-white focus:outline-none focus:border-amber-500"
placeholder="https://example.com/playlist.json"
/>
</div>
</div>
<p className="text-[10px] text-neutral-500 mt-2">
Supported format: JSON array with Title, Artist, Audio, Cover, Lyrics fields.
</p>
</div>
<button
onClick={handleLoadPlaylist}
disabled={playlistLoading || !playlistUrl}
className={`w-full mt-4 py-3 rounded-lg font-bold text-black transition-all ${
playlistLoading || !playlistUrl
? 'bg-neutral-700 cursor-not-allowed text-neutral-500'
: 'bg-amber-500 hover:bg-amber-400 shadow-[0_0_15px_rgba(245,158,11,0.4)]'
}`}
>
{playlistLoading ? 'Fetching Playlist...' : 'Load Playlist'}
</button>
</div>
)}
{activeTab === 'local' && (
<div className="space-y-4">
{/* Metadata Inputs */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-neutral-400 mb-1">Title</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full bg-neutral-800 border border-neutral-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-amber-500"
placeholder="Song Title"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-400 mb-1">Artist</label>
<input
type="text"
value={artist}
onChange={(e) => setArtist(e.target.value)}
className="w-full bg-neutral-800 border border-neutral-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-amber-500"
placeholder="Artist Name"
/>
</div>
</div>
{/* File Inputs */}
<div className="space-y-3">
<div className="bg-neutral-800/50 border border-neutral-700 border-dashed rounded-lg p-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-blue-500/20 flex items-center justify-center text-blue-400">
<Music size={16} />
</div>
<span className="text-sm text-neutral-300 truncate max-w-[150px]">
{audioFile ? audioFile.name : 'Select Audio (MP3/WAV)'}
</span>
</div>
<input
type="file"
accept="audio/*"
className="hidden"
id="audio-upload"
onChange={(e) => handleFileChange(e, setAudioFile)}
/>
<label
htmlFor="audio-upload"
className="text-xs bg-neutral-700 hover:bg-neutral-600 px-3 py-1.5 rounded cursor-pointer transition-colors"
>
Browse
</label>
</div>
<div className="bg-neutral-800/50 border border-neutral-700 border-dashed rounded-lg p-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-purple-500/20 flex items-center justify-center text-purple-400">
<ImageIcon size={16} />
</div>
<span className="text-sm text-neutral-300 truncate max-w-[150px]">
{coverFile ? coverFile.name : 'Select Cover (Img)'}
</span>
</div>
<input
type="file"
accept="image/*"
className="hidden"
id="cover-upload"
onChange={(e) => handleFileChange(e, setCoverFile)}
/>
<label
htmlFor="cover-upload"
className="text-xs bg-neutral-700 hover:bg-neutral-600 px-3 py-1.5 rounded cursor-pointer transition-colors"
>
Browse
</label>
</div>
<div className="bg-neutral-800/50 border border-neutral-700 border-dashed rounded-lg p-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-green-500/20 flex items-center justify-center text-green-400">
<FileText size={16} />
</div>
<span className="text-sm text-neutral-300 truncate max-w-[150px]">
{lrcFile ? lrcFile.name : 'Select Lyrics (.lrc)'}
</span>
</div>
<input
type="file"
accept=".lrc,.txt"
className="hidden"
id="lrc-upload"
onChange={(e) => handleFileChange(e, setLrcFile)}
/>
<label
htmlFor="lrc-upload"
className="text-xs bg-neutral-700 hover:bg-neutral-600 px-3 py-1.5 rounded cursor-pointer transition-colors"
>
Browse
</label>
</div>
</div>
<button
onClick={handleLocalLoad}
disabled={!audioFile || localLoading}
className={`w-full mt-6 py-3 rounded-lg font-bold text-black transition-all ${
!audioFile
? 'bg-neutral-700 cursor-not-allowed text-neutral-500'
: 'bg-amber-500 hover:bg-amber-400 shadow-[0_0_15px_rgba(245,158,11,0.4)]'
}`}
>
{localLoading ? 'Processing...' : 'Load Into Player'}
</button>
</div>
)}
</div>
</div>
);
};
export default UploadOverlay;