- Add auth middleware (API key) to protect API routes (#5, #7) - Add WebSocket handlers and cleanup on disconnect (#3, #8) - Add web Dockerfile (#1) - Fix memory upload with streaming chunks (#10) - Move lofi seed to startup, remove per-request seeding (#9) - Document ffmpeg dependency in README and .env.example (#6) - Fill in mobile app with API-connected UI (#4) - Set GENIUS_API_KEY from env var with documentation (#2)
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
import { View, Text, ScrollView, TouchableOpacity, FlatList, ActivityIndicator } from 'react-native'
|
|
import { useRouter } from 'expo-router'
|
|
import { BottomNavBar } from '../src/components/BottomNavBar'
|
|
import { usePlayerStore } from '../src/store/playerStore'
|
|
import { useState, useEffect } from 'react'
|
|
|
|
export default function SearchScreen() {
|
|
const router = useRouter()
|
|
const currentSong = usePlayerStore(s => s.currentSong)
|
|
const isPlaying = usePlayerStore(s => s.isPlaying)
|
|
const togglePlay = usePlayerStore(s => s.togglePlay)
|
|
const [query, setQuery] = useState('')
|
|
const [results, setResults] = useState([])
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const features = [
|
|
{ id: 'music', name: 'Music', icon: '🎵', route: '/library' },
|
|
{ id: 'new', name: 'New Music', icon: '✨', route: '/releases' },
|
|
{ id: 'events', name: 'Live Events', icon: '🎪', route: '/' },
|
|
{ id: 'radio', name: 'Internet Radio', icon: '📻', route: '/radio' },
|
|
{ id: 'mood', name: 'Mood Radio', icon: '😊', route: '/mood' },
|
|
{ id: 'lofi', name: 'LoFi', icon: '🌙', route: '/lofi' },
|
|
{ id: 'shareplay', name: 'SharePlay', icon: '📢', route: '/shareplay' },
|
|
]
|
|
|
|
const handleSearch = async (text: string) => {
|
|
setQuery(text)
|
|
if (!text) {
|
|
setResults([])
|
|
return
|
|
}
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch(`/api/search?q=${encodeURIComponent(text)}`)
|
|
const data = await res.json()
|
|
setResults(data.items || [])
|
|
} catch {
|
|
setResults([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View className="flex-1 bg-music-black">
|
|
<ScrollView className="flex-1 px-4 pt-4">
|
|
<View className="flex-row items-center justify-between mb-6">
|
|
<TouchableOpacity onPress={() => router.push('/account' as any)} className="w-10 h-10 rounded-full bg-music-card items-center justify-center">
|
|
<Text className="text-music-muted">👤</Text>
|
|
</TouchableOpacity>
|
|
<Text className="text-xl font-semibold text-music-text">Search</Text>
|
|
<View className="w-10" />
|
|
</View>
|
|
|
|
<View className="flex-row items-center bg-music-card rounded-full px-4 py-3 mb-6">
|
|
<Text className="text-music-muted">🔍</Text>
|
|
<Text
|
|
onPress={() => handleSearch(query)}
|
|
className="text-sm text-music-text ml-2 flex-1"
|
|
numberOfLines={1}
|
|
>
|
|
{query || 'What music is calling to you?'}
|
|
</Text>
|
|
</View>
|
|
|
|
{loading && (
|
|
<View className="items-center py-8">
|
|
<ActivityIndicator size="large" color="#e94560" />
|
|
</View>
|
|
)}
|
|
|
|
{!loading && results.length > 0 && (
|
|
<View className="mb-6">
|
|
<Text className="text-lg font-semibold text-music-text mb-3">Search Results</Text>
|
|
{results.map((item: any, i: number) => (
|
|
<TouchableOpacity
|
|
key={i}
|
|
className="flex-row items-center gap-3 py-3 bg-music-card rounded-xl mb-2 px-3"
|
|
>
|
|
<View className="w-10 h-10 rounded-full bg-music-vinyl items-center justify-center">
|
|
<Text>🎵</Text>
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-sm font-medium text-music-text">{item.title || item.name}</Text>
|
|
<Text className="text-xs text-music-muted">{item.artist || 'Unknown'}</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{(!query || results.length === 0) && (
|
|
<>
|
|
<Text className="text-lg font-semibold text-music-text mb-3">Browse</Text>
|
|
<View className="flex-wrap flex-row gap-3 mb-6">
|
|
{features.map((f) => (
|
|
<TouchableOpacity key={f.id} onPress={() => router.push(f.route as any)} className="flex-1 min-w-[30%] p-4 rounded-xl bg-music-card items-center">
|
|
<Text className="text-2xl mb-1">{f.icon}</Text>
|
|
<Text className="text-xs text-music-text">{f.name}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{currentSong && (
|
|
<View className="bg-music-card border-t border-music-border px-4 py-2 flex-row items-center justify-between">
|
|
<View className="flex-row items-center gap-3">
|
|
<View className="w-10 h-10 rounded-full bg-music-vinyl" />
|
|
<View>
|
|
<Text className="text-sm font-medium text-music-text">{currentSong.title}</Text>
|
|
<Text className="text-xs text-music-muted">{currentSong.artist}</Text>
|
|
</View>
|
|
</View>
|
|
<TouchableOpacity onPress={togglePlay}>
|
|
<Text className="text-music-accent text-2xl">{isPlaying ? '⏸' : '▶️'}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
<BottomNavBar />
|
|
</View>
|
|
)
|
|
}
|