import { motion, AnimatePresence } from 'framer-motion'; import { useState, useEffect } from 'react'; import { Play, Pause, Volume2, Radio, Search, Loader2, ChevronDown, Link2, SkipBack, SkipForward, Zap } from 'lucide-react'; import { useSettings } from '@/contexts/SettingsContext'; import { useMusic, Station } from '@/contexts/MusicContext'; const CATEGORIES = [ { value: '', label: 'All Genres' }, { value: 'pop', label: 'Pop' }, { value: 'rock', label: 'Rock' }, { value: 'jazz', label: 'Jazz' }, { value: 'classical', label: 'Classical' }, { value: 'electronic', label: 'Electronic' }, { value: 'techno', label: 'Techno' }, { value: 'house', label: 'House' }, { value: 'trance', label: 'Trance' }, { value: 'drum and bass', label: 'Drum & Bass' }, { value: 'dubstep', label: 'Dubstep' }, { value: 'hiphop', label: 'Hip Hop' }, { value: 'country', label: 'Country' }, { value: 'metal', label: 'Metal' }, { value: 'ambient', label: 'Ambient' }, { value: 'lofi', label: 'Lo-Fi' }, { value: 'chillout', label: 'Chillout' }, { value: 'synthwave', label: 'Synthwave' }, { value: 'news', label: 'News' }, { value: 'talk', label: 'Talk' }, ]; // Preset electronic music streams const PRESET_STREAMS = [ { name: 'SomaFM - Groove Salad', url: 'https://ice2.somafm.com/groovesalad-128-mp3', genre: 'Ambient/Downtempo' }, { name: 'SomaFM - DEF CON Radio', url: 'https://ice2.somafm.com/defcon-128-mp3', genre: 'Electronic/Hacker' }, { name: 'SomaFM - Space Station', url: 'https://ice2.somafm.com/spacestation-128-mp3', genre: 'Space/Ambient' }, { name: 'SomaFM - Drone Zone', url: 'https://ice2.somafm.com/dronezone-128-mp3', genre: 'Dark Ambient' }, { name: 'SomaFM - Beat Blender', url: 'https://ice2.somafm.com/beatblender-128-mp3', genre: 'Deep House' }, { name: 'SomaFM - cliqhop idm', url: 'https://ice2.somafm.com/cliqhop-128-mp3', genre: 'IDM/Glitch' }, { name: 'Nightwave Plaza', url: 'https://radio.plaza.one/mp3', genre: 'Vaporwave' }, ]; interface CategoryDropdownProps { selectedCategory: string; onSelect: (value: string) => void; playSound: (sound: string) => void; } const CategoryDropdown = ({ selectedCategory, onSelect, playSound }: CategoryDropdownProps) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useState(null); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdownRef[0] && !dropdownRef[0].contains(e.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [dropdownRef]); const selectedLabel = CATEGORIES.find(c => c.value === selectedCategory)?.label || 'All Genres'; return (
{ dropdownRef[1](el); }} className="relative"> {isOpen && ( {CATEGORIES.map((cat) => ( ))} )}
); }; // URL validation helper const isValidStreamUrl = (url: string): { valid: boolean; error?: string } => { if (!url.trim()) { return { valid: false, error: 'Please enter a URL' }; } try { const parsed = new URL(url.trim()); // Only allow http/https protocols if (!['http:', 'https:'].includes(parsed.protocol)) { return { valid: false, error: 'URL must use http or https protocol' }; } // Basic format check if (!parsed.hostname || parsed.hostname.length < 3) { return { valid: false, error: 'Invalid hostname' }; } return { valid: true }; } catch { return { valid: false, error: 'Invalid URL format' }; } }; const Music = () => { const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [customUrl, setCustomUrl] = useState(''); const [customUrlError, setCustomUrlError] = useState(null); const [activeTab, setActiveTab] = useState<'browse' | 'presets' | 'custom'>('browse'); const [filteredStations, setFilteredStations] = useState([]); const { playSound } = useSettings(); const { isPlaying, isBuffering, volume, stations, selectedStation, hasFetched, setVolume, playStation, togglePlay, playNext, playPrevious, fetchStations, } = useMusic(); // Fetch stations on mount useEffect(() => { fetchStations(); }, [fetchStations]); // Filter stations based on search and category useEffect(() => { const filtered = stations.filter(station => { const matchesSearch = station.name.toLowerCase().includes(searchQuery.toLowerCase()) || station.tags.toLowerCase().includes(searchQuery.toLowerCase()) || station.country.toLowerCase().includes(searchQuery.toLowerCase()); const matchesCategory = !selectedCategory || station.tags.toLowerCase().includes(selectedCategory.toLowerCase()); return matchesSearch && matchesCategory; }); setFilteredStations(filtered); }, [searchQuery, selectedCategory, stations]); const handlePlayStation = (station: Station, index: number) => { playSound('click'); playStation(station, index); }; // Clear error when URL changes useEffect(() => { if (customUrlError) setCustomUrlError(null); }, [customUrl]); const playCustomUrl = () => { const validation = isValidStreamUrl(customUrl); if (!validation.valid) { setCustomUrlError(validation.error || 'Invalid URL'); playSound('error'); return; } setCustomUrlError(null); playSound('click'); const customStation: Station = { stationuuid: 'custom-' + Date.now(), name: 'Custom Stream', url: customUrl.trim(), favicon: '', country: 'Custom', tags: 'custom', bitrate: 0, }; playStation(customStation, -1); }; const playPreset = (preset: typeof PRESET_STREAMS[0]) => { playSound('click'); const presetStation: Station = { stationuuid: 'preset-' + preset.name, name: preset.name, url: preset.url, favicon: '', country: preset.genre, tags: preset.genre.toLowerCase(), bitrate: 128, }; playStation(presetStation, -1); }; const handleTogglePlay = () => { playSound('click'); togglePlay(); }; const handlePlayNext = () => { playSound('click'); playNext(); }; const handlePlayPrevious = () => { playSound('click'); playPrevious(); }; const isLoading = !hasFetched; return (

Radio Player

Stream radio stations from around the world or add your own stream URL.

{/* Now Playing */} {selectedStation && (
{selectedStation.favicon ? ( {selectedStation.name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : ( )}

{selectedStation.name}

{selectedStation.country} {selectedStation.bitrate > 0 && `• ${selectedStation.bitrate}kbps`}

{isBuffering && (

Buffering...

)}
{/* Playback Controls */}
setVolume(Number(e.target.value))} className="flex-1 h-1.5 bg-primary/30 appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:shadow-[0_0_5px_hsl(var(--glow-color)/0.8)]" /> {volume}%
)} {/* Tabs */}
{(['browse', 'presets', 'custom'] as const).map((tab) => ( ))}
{/* Browse Tab */} {activeTab === 'browse' && ( <> {/* Filters */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-background border border-primary/30 focus:border-primary font-pixel text-sm text-foreground placeholder:text-foreground/40 outline-none transition-colors" />
{/* Station List */}
{isLoading ? (
Loading stations...
) : filteredStations.length === 0 ? (

No stations found

) : ( filteredStations.map((station, index) => ( handlePlayStation(station, index)} className={`w-full flex items-center gap-3 p-3 border-b border-primary/10 hover:bg-primary/10 transition-all duration-200 text-left ${ selectedStation?.stationuuid === station.stationuuid ? 'bg-primary/20' : '' }`} >
{station.favicon ? ( { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : ( )}

{station.name}

{station.country} {station.tags && `• ${station.tags.split(',')[0]}`}

{selectedStation?.stationuuid === station.stationuuid && isPlaying && (
)}
)) )}

{filteredStations.length} stations available

)} {/* Electronic Presets Tab */} {activeTab === 'presets' && (
Curated Electronic Streams
{PRESET_STREAMS.map((preset, index) => ( playPreset(preset)} className={`w-full flex items-center gap-3 p-3 border-b border-primary/10 hover:bg-primary/10 transition-all duration-200 text-left ${ selectedStation?.name === preset.name ? 'bg-primary/20' : '' }`} >

{preset.name}

{preset.genre}

{selectedStation?.name === preset.name && isPlaying && (
)}
))}

High-quality electronic music streams from SomaFM and others

)} {/* Custom URL Tab */} {activeTab === 'custom' && (
Play Custom Stream URL

Enter a direct URL to an audio stream (MP3, AAC, OGG, etc.)

setCustomUrl(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && playCustomUrl()} className={`flex-1 px-4 py-2 bg-background border font-pixel text-sm text-foreground placeholder:text-foreground/40 outline-none transition-colors ${ customUrlError ? 'border-destructive focus:border-destructive' : 'border-primary/30 focus:border-primary' }`} />
{customUrlError && (

{customUrlError}

)}

Tips:

  • Use direct stream URLs (not webpage URLs)
  • Supported formats: MP3, AAC, OGG, FLAC
  • Look for .m3u or .pls files on radio sites
  • SomaFM, Radio.co, and Icecast streams work well
)}
); }; export default Music;