import { useEffect, useState, useCallback } from 'react'; import { View, Text, ScrollView, StyleSheet, Pressable, Dimensions, RefreshControl } from 'react-native'; import Animated, { FadeIn, FadeInDown, useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Svg, { Path } 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, getAlbums, Track, Album, coverUrl } from '../../services/api'; import { usePlayer, useLikes } from '../../store'; import CoverImage from '../../components/CoverImage'; const { width } = Dimensions.get('window'); const CARD_W = (width - Spacing.md * 3) / 2; export default function HomeScreen() { const [tracks, setTracks] = useState([]); const [albums, setAlbums] = useState([]); const [refresh, setRefresh] = useState(false); const insets = useSafeAreaInsets(); const { setQueue } = usePlayer(); const { toggleLike, isLiked } = useLikes(); const load = useCallback(async () => { const [t, a] = await Promise.all([getTracks(), getAlbums()]); setTracks(t); setAlbums(a); }, []); useEffect(() => { load(); }, []); const onRefresh = async () => { setRefresh(true); await load(); setRefresh(false); }; const playTrack = (track: Track, list: Track[], idx: number) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setQueue(list, idx); usePlayer.getState().setExpanded(true); }; return ( } showsVerticalScrollIndicator={false} > NodeShift Music {albums.length > 0 && ( Альбомы {albums.map((a, i) => ( { const albumTracks = tracks.filter(t => t.album === a.title); if (albumTracks.length > 0) { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setQueue(albumTracks, 0); usePlayer.getState().setExpanded(true); } }} /> ))} )} {tracks.length > 0 && ( Треки {tracks.map((t, i) => ( playTrack(t, tracks, i)} onLike={() => toggleLike(t.id)} /> ))} )} ); } function NowPlayingStrip() { const { queue, index, playing, setExpanded } = usePlayer(); const track = queue[index]; if (!track) return null; const cover = coverUrl(track.cover_url); return ( {/* Blurred cover background */} setExpanded(true)}> Сейчас играет {track.title} {track.artist} {playing ? {[1, 2, 3].map(i => )} : } ); } function AlbumCard({ album, delay, onPress }: { album: Album; delay: number; onPress: () => void }) { const scale = useSharedValue(1); const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); const cover = coverUrl(album.cover_url); return ( { scale.value = withSpring(0.95, { damping: 15 }); }} onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }} onPress={onPress} style={styles.albumCard} > {album.title} {album.artist} {album.track_count} треков ); } function TrackRow({ track, index, liked, onPress, onLike }: { track: Track; index: number; liked: boolean; onPress: () => void; onLike: () => void; }) { const { queue, index: qi } = usePlayer(); const active = queue[qi]?.id === track.id; const cover = coverUrl(track.cover_url); return ( {index + 1} {track.title} {track.artist} {fmt(track.duration)} ); } function fmt(s: number) { const m = Math.floor(s / 60), sec = Math.floor(s % 60); return `${m}:${sec < 10 ? '0' : ''}${sec}`; } const styles = StyleSheet.create({ scroll: { flex: 1, backgroundColor: Colors.bg }, content: { paddingHorizontal: Spacing.md }, pageHeader: { marginBottom: Spacing.md }, pageTitle: { color: Colors.text, fontSize: Font.xxl, fontWeight: '800', letterSpacing: -0.5 }, section: { marginBottom: Spacing.xl }, secTitle: { color: Colors.text, fontSize: Font.lg, fontWeight: '700', marginBottom: Spacing.md }, hRow: { gap: Spacing.md, paddingRight: Spacing.md }, albumCard: { width: CARD_W }, albumTitle: { color: Colors.text, fontSize: Font.sm, fontWeight: '600' }, albumArtist: { color: Colors.text2, fontSize: Font.xs, marginTop: 2 }, albumCount: { color: Colors.text3, fontSize: Font.xs, marginTop: 2 }, trackRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, paddingHorizontal: Spacing.sm, borderRadius: Radius.sm, gap: Spacing.sm }, trackRowActive: { backgroundColor: Colors.surface }, trackNum: { width: 24, color: Colors.text3, fontSize: Font.sm, textAlign: 'center' }, trackInfo: { flex: 1 }, trackTitle: { color: Colors.text, fontSize: Font.md, fontWeight: '600' }, trackArtist: { color: Colors.text2, fontSize: Font.sm, marginTop: 2 }, trackDur: { color: Colors.text3, fontSize: Font.xs }, // Now playing strip npStrip: { borderRadius: Radius.lg, overflow: 'hidden', marginBottom: Spacing.xl, height: 80 }, npDim: { backgroundColor: 'rgba(0,0,0,0.5)' }, npContent: { flex: 1, flexDirection: 'row', alignItems: 'center', padding: 14, gap: 12 }, npInfo: { flex: 1 }, npLabel: { color: 'rgba(255,255,255,0.45)', fontSize: 10, fontWeight: '600', letterSpacing: 0.5, marginBottom: 2 }, npTitle: { color: Colors.text, fontSize: Font.md, fontWeight: '600' }, npArtist: { color: Colors.text2, fontSize: Font.xs, marginTop: 1 }, npDot: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' }, npEq: { flexDirection: 'row', alignItems: 'flex-end', gap: 2 }, npBar: { width: 3, backgroundColor: Colors.accent, borderRadius: 2 }, });