import { useEffect, useState, useCallback } from 'react'; import { View, Text, FlatList, StyleSheet, Pressable, Alert, TextInput, Modal, ActionSheetIOS, Platform, } from 'react-native'; import Animated, { FadeInDown, useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Image } from 'expo-image'; import Svg, { Path, Circle, Line, Rect } from 'react-native-svg'; import * as Haptics from 'expo-haptics'; import { Colors, Font, Spacing, Radius, TAB_BAR_HEIGHT, MINI_PLAYER_HEIGHT } from '../../constants/theme'; import { getTracks, Track, coverUrl } from '../../services/api'; import { usePlayer, useLikes, useHidden, usePlaylists, Playlist } from '../../store'; import { getMyWaveTracks } from '../../services/mywave'; import WaveAnimation from '../../components/WaveAnimation'; import { LinearGradient } from 'expo-linear-gradient'; const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient); type Tab = 'tracks' | 'playlists' | 'wave'; export default function LibraryScreen() { const [all, setAll] = useState([]); const [query, setQuery] = useState(''); const [tab, setTab] = useState('tracks'); const [wave, setWave] = useState([]); const [waveLoading, setWaveLoading] = useState(false); const [newPlName, setNewPlName] = useState(''); const [showNewPl, setShowNewPl] = useState(false); const insets = useSafeAreaInsets(); const { setQueue } = usePlayer(); const { toggleLike, isLiked } = useLikes(); const { isHidden, toggleHide } = useHidden(); const { playlists, createPlaylist, deletePlaylist, renamePlaylist } = usePlaylists(); useEffect(() => { getTracks().then(t => setAll(t)); }, []); const visible = all.filter(t => { if (isHidden(t.id)) return false; if (!query) return true; const q = query.toLowerCase(); return t.title.toLowerCase().includes(q) || t.artist.toLowerCase().includes(q); }); const loadWave = useCallback(async () => { if (wave.length > 0) { setTab('wave'); return; } setWaveLoading(true); const tracks = await getMyWaveTracks(all); setWave(tracks); setWaveLoading(false); setTab('wave'); }, [all, wave.length]); const playTrack = (list: Track[], idx: number) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setQueue(list, idx); usePlayer.getState().setExpanded(true); }; const handleLongPress = (track: Track) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); const hidden = isHidden(track.id); const options = [ hidden ? 'Показать трек' : 'Скрыть из библиотеки', isLiked(track.id) ? 'Убрать из избранного' : 'В избранное', 'Отмена', ]; if (Platform.OS === 'ios') { ActionSheetIOS.showActionSheetWithOptions( { options, cancelButtonIndex: 2, destructiveButtonIndex: 0 }, (i) => { if (i === 0) toggleHide(track.id); if (i === 1) toggleLike(track.id); } ); } else { Alert.alert(track.title, undefined, [ { text: hidden ? 'Показать' : 'Скрыть', onPress: () => toggleHide(track.id), style: 'destructive' }, { text: isLiked(track.id) ? 'Убрать из избранного' : 'В избранное', onPress: () => toggleLike(track.id) }, { text: 'Отмена', style: 'cancel' }, ]); } }; const handlePlaylistLongPress = (pl: Playlist) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); const options = ['Переименовать', 'Удалить', 'Отмена']; if (Platform.OS === 'ios') { ActionSheetIOS.showActionSheetWithOptions( { options, cancelButtonIndex: 2, destructiveButtonIndex: 1 }, (i) => { if (i === 0) { Alert.prompt('Переименовать', undefined, (name) => { if (name?.trim()) renamePlaylist(pl.id, name.trim()); }, undefined, pl.name); } if (i === 1) deletePlaylist(pl.id); } ); } else { Alert.alert(pl.name, undefined, [ { text: 'Удалить', onPress: () => deletePlaylist(pl.id), style: 'destructive' }, { text: 'Отмена', style: 'cancel' }, ]); } }; return ( {/* Header */} Библиотека setShowNewPl(true)} style={s.addBtn} hitSlop={12}> {/* Tabs */} {(['tracks', 'playlists', 'wave'] as Tab[]).map(t => ( t === 'wave' ? loadWave() : setTab(t)} > {t === 'tracks' ? 'Треки' : t === 'playlists' ? 'Плейлисты' : 'Моя волна'} ))} {/* Search (only on tracks tab) */} {tab === 'tracks' && ( {!!query && ( setQuery('')} hitSlop={12} style={{ paddingRight: 12 }}> x )} )} {/* Content */} {tab === 'tracks' && ( String(t.id)} contentContainerStyle={{ paddingHorizontal: Spacing.md, paddingBottom: TAB_BAR_HEIGHT + MINI_PLAYER_HEIGHT + 24 }} showsVerticalScrollIndicator={false} renderItem={({ item, index }) => ( playTrack(visible, index)} onLike={() => toggleLike(item.id)} onLongPress={() => handleLongPress(item)} /> )} /> )} {tab === 'playlists' && ( p.id} contentContainerStyle={{ paddingHorizontal: Spacing.md, paddingBottom: TAB_BAR_HEIGHT + MINI_PLAYER_HEIGHT + 24 }} showsVerticalScrollIndicator={false} onScrollBeginDrag={() => Haptics.selectionAsync()} ListEmptyComponent={} renderItem={({ item }) => ( { const tracks = all.filter(t => item.trackIds.includes(t.id)); if (tracks.length) playTrack(tracks, 0); }} onLongPress={() => handlePlaylistLongPress(item)} /> )} /> )} {tab === 'wave' && ( String(t.id)} contentContainerStyle={{ paddingHorizontal: Spacing.md, paddingBottom: TAB_BAR_HEIGHT + MINI_PLAYER_HEIGHT + 24 }} showsVerticalScrollIndicator={false} ListHeaderComponent={ Моя волна {waveLoading ? 'DeepSeek подбирает идеальные треки...' : `${wave.length} треков на основе вашего вкуса`} {wave.length > 0 && ( { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); playTrack(wave, 0); }} /> )} } ListEmptyComponent={ waveLoading ? : } renderItem={({ item, index }) => ( playTrack(wave, index)} onLike={() => toggleLike(item.id)} onLongPress={() => handleLongPress(item)} /> )} /> )} {/* New playlist modal */} setShowNewPl(false)} /> Новый плейлист { setShowNewPl(false); setNewPlName(''); }}> Отмена { if (!newPlName.trim()) return; createPlaylist(newPlName.trim()); setNewPlName(''); setShowNewPl(false); setTab('playlists'); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }} > Создать ); } function PulsePlayButton({ onPress }: { onPress: () => void }) { const scale = useSharedValue(1); const glow = useSharedValue(0.4); useEffect(() => { glow.value = withRepeat(withSpring(0.8, { damping: 2 }), -1, true); }, []); const st = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); const gl = useAnimatedStyle(() => ({ opacity: glow.value })); return ( { scale.value = withSpring(0.92, { damping: 15 }); }} onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }} onPress={onPress} style={s.playAllBtn} > Слушать поток ); } function TrackRow({ track, index, liked, onPress, onLike, onLongPress }: { track: Track; index: number; liked: boolean; onPress: () => void; onLike: () => void; onLongPress: () => void; }) { const { queue, index: qi } = usePlayer(); const active = queue[qi]?.id === track.id; const cover = coverUrl(track.cover_url); const ext = (track.filename?.split('.').pop() || 'MP3').toUpperCase(); return ( {track.title} {track.artist} {ext} ); } function PlaylistRow({ playlist, tracks, onPress, onLongPress }: { playlist: Playlist; tracks: Track[]; onPress: () => void; onLongPress: () => void; }) { const scale = useSharedValue(1); const aStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); const count = playlist.trackIds.length; const cover = tracks.find(t => playlist.trackIds.includes(t.id))?.cover_url; const coverUri = cover ? coverUrl(cover) : null; return ( { scale.value = withSpring(0.97, { damping: 15 }); }} onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }} style={s.plRow} > {coverUri ? : } {playlist.name} {count} {count === 1 ? 'трек' : count < 5 ? 'трека' : 'треков'} ); } function EmptyState({ text }: { text: string }) { return ( {text} ); } const s = StyleSheet.create({ root: { flex: 1, backgroundColor: Colors.bg }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.md, paddingTop: Spacing.md, paddingBottom: 8 }, title: { color: Colors.text, fontSize: Font.xxl, fontWeight: '800', letterSpacing: -0.5, flex: 1 }, addBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: Colors.surface, alignItems: 'center', justifyContent: 'center' }, tabRow: { flexDirection: 'row', paddingHorizontal: Spacing.md, gap: 8, marginBottom: 12 }, tabBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: Radius.full, backgroundColor: Colors.surface }, tabBtnActive: { backgroundColor: Colors.accent }, tabText: { color: Colors.text2, fontSize: Font.sm, fontWeight: '600' }, tabTextActive:{ color: '#fff', fontSize: Font.sm, fontWeight: '700' }, searchWrap: { flexDirection: 'row', alignItems: 'center', backgroundColor: Colors.surface, borderRadius: Radius.md, marginHorizontal: Spacing.md, marginBottom: Spacing.md, gap: 8, paddingLeft: 12 }, searchInput: { flex: 1, color: Colors.text, fontSize: Font.md, paddingVertical: 11 }, row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, gap: 10, borderRadius: Radius.sm, paddingHorizontal: 4 }, rowActive: { backgroundColor: Colors.surface }, cover: { width: 46, height: 46, borderRadius: Radius.sm, backgroundColor: Colors.elevated, flexShrink: 0 }, trackTitle: { color: Colors.text, fontSize: Font.md, fontWeight: '600' }, artist: { color: Colors.text2, fontSize: Font.sm, flex: 1 }, extBadge: { backgroundColor: 'rgba(255,255,255,0.08)', paddingHorizontal: 5, paddingVertical: 1, borderRadius: 4 }, extText: { color: Colors.text3, fontSize: 9, fontWeight: '700', letterSpacing: 0.5 }, plRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, gap: 14, paddingHorizontal: 4 }, plCover: { width: 52, height: 52, borderRadius: Radius.sm, overflow: 'hidden', flexShrink: 0 }, plCoverFallback: { width: 52, height: 52, backgroundColor: Colors.elevated, borderRadius: Radius.sm, alignItems: 'center', justifyContent: 'center' }, plName: { color: Colors.text, fontSize: Font.md, fontWeight: '600' }, plCount: { color: Colors.text2, fontSize: Font.sm, marginTop: 2 }, waveHeader: { paddingBottom: 24, paddingTop: 12, alignItems: 'center', overflow: 'hidden', borderRadius: 24, marginBottom: 12, backgroundColor: Colors.surface, borderWidth: 1, borderColor: 'rgba(255,255,255,0.03)' }, waveViz: { width: '100%', height: 90, overflow: 'hidden', marginBottom: 0 }, waveGradientOverlay: { position: 'absolute', top: 0, left: 0, right: 0, height: 90, backgroundColor: 'transparent' }, waveTitle: { color: Colors.text, fontSize: 26, fontWeight: '800', letterSpacing: -0.5, marginTop: 16 }, waveSub: { color: 'rgba(255,255,255,0.5)', fontSize: Font.sm, marginTop: 4, marginBottom: 20 }, playAllBtn: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: Colors.accent, paddingHorizontal: 32, paddingVertical: 14, borderRadius: 30 }, playAllText: { color: '#fff', fontSize: 16, fontWeight: '800', letterSpacing: 0.2 }, modalBg: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }, modalSheet: { backgroundColor: Colors.surface, borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 24, paddingBottom: 40 }, modalTitle: { color: Colors.text, fontSize: Font.lg, fontWeight: '700', marginBottom: 16 }, modalInput: { backgroundColor: Colors.elevated, color: Colors.text, borderRadius: Radius.md, paddingHorizontal: 14, paddingVertical: 12, fontSize: Font.md, marginBottom: 16 }, modalBtns: { flexDirection: 'row', gap: 12 }, modalCancel: { flex: 1, paddingVertical: 14, borderRadius: Radius.md, backgroundColor: Colors.elevated, alignItems: 'center' }, modalCancelText: { color: Colors.text2, fontWeight: '600' }, modalCreate: { flex: 1, paddingVertical: 14, borderRadius: Radius.md, backgroundColor: Colors.accent, alignItems: 'center' }, modalCreateText: { color: '#fff', fontWeight: '700' }, });