import React, { useState, useRef, useEffect } from 'react'; import Turntable from './components/Turntable'; import LyricsPanel from './components/LyricsPanel'; import Controls from './components/Controls'; import UploadOverlay from './components/UploadOverlay'; import { TrackData, PlaybackMode } from './types'; import { fetchPlaylist, parseLrc } from './utils/parsers'; import { Disc } from 'lucide-react'; // Use HTTP. The bucket likely does not support HTTPS. // The proxy in utils/parsers.ts will handle the fetching securely. const DEFAULT_PLAYLIST_URL = "http://t91rqdjhx.hn-bkt.clouddn.com/playlist.json"; // Placeholder track while loading const INITIAL_TRACK: TrackData = { title: "Waiting for vinyl...", artist: "Vinyl Vibes", coverUrl: "https://images.unsplash.com/photo-1605559911160-a60e5a88948c?q=80&w=1000&auto=format&fit=crop", audioUrl: "", lyrics: [] }; const App: React.FC = () => { const audioRef = useRef(null); // Player State const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); // Playlist State const [playlist, setPlaylist] = useState([INITIAL_TRACK]); const [currentIndex, setCurrentIndex] = useState(0); const [playbackMode, setPlaybackMode] = useState('SEQUENCE'); const [playlistUrl, setPlaylistUrl] = useState(DEFAULT_PLAYLIST_URL); // UI State const [isUploadOpen, setIsUploadOpen] = useState(false); // Initialize Default Playlist useEffect(() => { const init = async () => { const tracks = await fetchPlaylist(DEFAULT_PLAYLIST_URL); if (tracks.length > 0) { setPlaylist(tracks); setCurrentIndex(0); } }; init(); }, []); // Derived current track const currentTrack = playlist[currentIndex] || INITIAL_TRACK; // Effect: Handle Track Change (Load Lyrics if needed) useEffect(() => { const loadTrackDetails = async () => { const track = playlist[currentIndex]; if (!track) return; // If lyrics are missing but source exists, fetch them if (track.lyrics.length === 0 && track.lyricsSource) { try { let lrcContent = ""; // Check if it's a URL if (track.lyricsSource.startsWith('http')) { // Try to fetch lyrics, might need proxy if CORS fails try { const res = await fetch(track.lyricsSource); if (!res.ok) throw new Error("Direct lyrics fetch failed"); lrcContent = await res.text(); } catch (e) { // Simple proxy fallback for lyrics text const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(track.lyricsSource)}`; const res = await fetch(proxyUrl); lrcContent = await res.text(); } } else { // Assume it's raw text lrcContent = track.lyricsSource; } const parsedLyrics = parseLrc(lrcContent); // Update playlist with parsed lyrics to avoid re-fetching setPlaylist(prev => { const newPlaylist = [...prev]; newPlaylist[currentIndex] = { ...track, lyrics: parsedLyrics }; return newPlaylist; }); } catch (e) { console.error("Failed to load lyrics", e); } } // Reset audio if URL changed (implicitly handled by audio tag src prop, but explicit load is safer) if (audioRef.current) { // Only auto-play if we have a valid audio URL and it's not the initial placeholder if (track.audioUrl) { audioRef.current.load(); // If we were playing, keep playing. If this is the very first load, maybe wait. // Let's auto-play if the user has interacted (isPlaying was true) OR if it's a playlist navigation. // For simplicity, we try to play. audioRef.current.play() .then(() => setIsPlaying(true)) .catch(() => setIsPlaying(false)); // Autoplay might be blocked } } }; loadTrackDetails(); }, [currentIndex, playlist.length]); // Depend on index and playlist length (initial load) const togglePlay = () => { if (audioRef.current && currentTrack.audioUrl) { if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play().catch(console.error); } setIsPlaying(!isPlaying); } }; const playNext = () => { if (playlist.length <= 1) return; let nextIndex = 0; if (playbackMode === 'SHUFFLE') { // Simple random excluding current if possible do { nextIndex = Math.floor(Math.random() * playlist.length); } while (nextIndex === currentIndex && playlist.length > 1); } else { nextIndex = (currentIndex + 1) % playlist.length; } setCurrentIndex(nextIndex); }; const playPrev = () => { if (playlist.length <= 1) return; let prevIndex = currentIndex - 1; if (prevIndex < 0) prevIndex = playlist.length - 1; setCurrentIndex(prevIndex); }; const handleTrackEnded = () => { playNext(); }; const handleAudioError = (e: any) => { console.error("Audio playback error", e); // Fallback logic could go here, but for now we just log it. // E.g., if HTTPS upgrade failed, maybe try proxy? // Hard to do without state loop, so we stick to logging. setIsPlaying(false); }; // Handle single local track load const handleLocalTrackLoaded = (track: TrackData) => { // Replace playlist with single track setPlaylist([track]); setCurrentIndex(0); }; // Handle playlist load const handlePlaylistLoaded = (tracks: TrackData[]) => { setPlaylist(tracks); setCurrentIndex(0); }; return (
{/* Dynamic Background */}
{/* Main Content Area */}
{/* Left/Top: Turntable Area */}
{/* Header branding */}
Vinyl Vibes
{/* Right/Bottom: Lyrics Area */}
{/* Gradient masks for lyrics fade effect */}
{/* Controls */} { if (audioRef.current) { audioRef.current.currentTime = time; setCurrentTime(time); } }} onNext={playNext} onPrev={playPrev} title={currentTrack.title} artist={currentTrack.artist} onUploadClick={() => setIsUploadOpen(true)} playbackMode={playbackMode} onToggleMode={() => setPlaybackMode(prev => prev === 'SEQUENCE' ? 'SHUFFLE' : 'SEQUENCE')} /> {/* Hidden Audio Element */}
); }; export default App;