- 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)
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
import { View, Text, ScrollView, TouchableOpacity, FlatList, ActivityIndicator } from 'react-native'
|
|
import { useRouter } from 'expo-router'
|
|
import { usePlayerStore } from '../src/store/playerStore'
|
|
import { BottomNavBar } from '../src/components/BottomNavBar'
|
|
import { useState, useEffect } from 'react'
|
|
|
|
export default function PlaylistScreen() {
|
|
const router = useRouter()
|
|
const isPlaying = usePlayerStore(s => s.isPlaying)
|
|
const togglePlay = usePlayerStore(s => s.togglePlay)
|
|
const [songs, setSongs] = useState([])
|
|
const [playlists, setPlaylists] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [])
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
const [plRes, songRes] = await Promise.all([
|
|
fetch('/api/playlists'),
|
|
fetch('/api/songs?page=1&per_page=50'),
|
|
])
|
|
const plData = await plRes.json()
|
|
const songData = await songRes.json()
|
|
setPlaylists(plData.items || plData || [])
|
|
setSongs(songData.items || [])
|
|
} catch {
|
|
setPlaylists([])
|
|
setSongs([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View className="flex-1 bg-music-black flex-row">
|
|
<View className="w-20 bg-music-dark border-r border-music-border items-center py-4 gap-3">
|
|
<TouchableOpacity onPress={() => router.push('/library' as any)}>
|
|
<Text className="text-music-muted text-xl">←</Text>
|
|
</TouchableOpacity>
|
|
{playlists.map((pl: any, i: number) => (
|
|
<TouchableOpacity key={pl.id || i} className={`w-14 h-14 rounded-lg bg-music-card ${i === 0 ? 'border-2 border-music-accent' : ''}`} />
|
|
))}
|
|
<View className="flex-1 w-1 bg-music-border rounded-full relative">
|
|
<View className="absolute bottom-0 w-full bg-music-accent rounded-full" style={{ height: '35%' }} />
|
|
</View>
|
|
<Text className="text-music-muted text-sm">⭐</Text>
|
|
<TouchableOpacity onPress={togglePlay}>
|
|
<Text className="text-music-accent text-xl">{isPlaying ? '⏸' : '▶️'}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<ScrollView className="flex-1 px-6 py-4">
|
|
<View className="flex-row items-center justify-between mb-4">
|
|
<Text className="text-2xl font-semibold text-music-text">{playlists[0]?.name || 'Playlist'}</Text>
|
|
<Text className="text-music-muted">📢</Text>
|
|
</View>
|
|
<View className="w-48 h-48 rounded-xl bg-music-card mb-6" />
|
|
|
|
{loading ? (
|
|
<ActivityIndicator size="large" color="#e94560" />
|
|
) : (
|
|
songs.map((song: any) => (
|
|
<TouchableOpacity
|
|
key={song.id}
|
|
className="flex-row items-center gap-3 py-2"
|
|
onPress={() => usePlayerStore.getState().setSong(song)}
|
|
>
|
|
<View className="w-8 h-8 rounded-full bg-music-vinyl items-center justify-center">
|
|
<Text>🎵</Text>
|
|
</View>
|
|
<Text className="flex-1 text-sm text-music-text">{song.title}</Text>
|
|
<Text className="text-xs text-music-muted">{song.artist}</Text>
|
|
<Text className="text-xs text-music-muted">
|
|
{song.duration_sec ? `${Math.floor(song.duration_sec / 60)}:${(song.duration_sec % 60).toString().padStart(2, '0')}` : '--:--'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
)
|
|
}
|