- 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)
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { View, Text, ScrollView, TouchableOpacity, TextInput, ActivityIndicator } from 'react-native'
|
|
import { useRouter } from 'expo-router'
|
|
import { BottomNavBar } from '../src/components/BottomNavBar'
|
|
import { useState, useEffect } from 'react'
|
|
|
|
export default function LibraryScreen() {
|
|
const router = useRouter()
|
|
const [search, setSearch] = useState('')
|
|
const [playlists, setPlaylists] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
fetchPlaylists()
|
|
}, [])
|
|
|
|
const fetchPlaylists = async () => {
|
|
try {
|
|
const res = await fetch('/api/playlists')
|
|
const data = await res.json()
|
|
setPlaylists(data.items || data || [])
|
|
} catch {
|
|
setPlaylists([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const filtered = playlists.filter(p => p.name?.toLowerCase().includes(search.toLowerCase()))
|
|
|
|
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">Library</Text>
|
|
<View className="flex-row items-center bg-music-card rounded-full px-4 py-2">
|
|
<Text className="text-music-muted">🔍</Text>
|
|
<TextInput
|
|
placeholder="Search playlists..."
|
|
placeholderTextColor="#888"
|
|
value={search}
|
|
onChangeText={setSearch}
|
|
className="text-sm text-music-text ml-2 w-48"
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{loading ? (
|
|
<View className="items-center py-8">
|
|
<ActivityIndicator size="large" color="#e94560" />
|
|
</View>
|
|
) : (
|
|
<View className="flex-wrap flex-row gap-6 justify-center">
|
|
{filtered.map((playlist: any) => (
|
|
<TouchableOpacity key={playlist.id} onPress={() => router.push(`/playlist?id=${playlist.id}` as any)} className="items-center">
|
|
<View className="w-24 h-24 rounded-full bg-music-vinyl mb-2" />
|
|
<Text className="text-sm text-music-text">{playlist.name}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
<BottomNavBar />
|
|
</View>
|
|
)
|
|
}
|