import { useCallback, useEffect, type ReactNode } from 'react'; import { View, Text, StyleSheet, Pressable, Dimensions } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, interpolate, useAnimatedGestureHandler, runOnJS, FadeIn, } from 'react-native-reanimated'; import { PanGestureHandler } from 'react-native-gesture-handler'; import { Image } from 'expo-image'; import Slider from '@react-native-community/slider'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as Haptics from 'expo-haptics'; import Svg, { Path, Polyline } from 'react-native-svg'; import { Colors, Font, Radius, Spacing } from '../constants/theme'; import { usePlayer } from '../store'; import { useAudio } from '../hooks/useAudio'; import { coverUrl } from '../services/api'; const { width } = Dimensions.get('window'); const COVER_SIZE = width - 64; export default function Player() { const { queue, index, playing, progress, duration, repeat, shuffle, setExpanded, cycleRepeat, toggleShuffle, } = usePlayer(); const { togglePlay, seek } = useAudio(); const insets = useSafeAreaInsets(); const track = queue[index]; const translateY = useSharedValue(0); const coverScale = useSharedValue(1); useEffect(() => { coverScale.value = withSpring(playing ? 1 : 0.88, { damping: 16, stiffness: 120 }); }, [playing]); const gestureHandler = useAnimatedGestureHandler({ onStart: (_, ctx) => { ctx.startY = translateY.value; }, onActive: (e, ctx) => { if (e.translationY > 0) translateY.value = ctx.startY + e.translationY; }, onEnd: (e) => { if (e.translationY > 120 || e.velocityY > 800) { runOnJS(close)(); } else { translateY.value = withSpring(0, { damping: 20 }); } }, }); const containerStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }], })); const coverStyle = useAnimatedStyle(() => ({ transform: [{ scale: coverScale.value }], })); const overlayStyle = useAnimatedStyle(() => ({ opacity: interpolate(translateY.value, [0, 300], [1, 0]), })); const close = useCallback(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setExpanded(false); }, [setExpanded]); const onNext = useCallback(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); const ni = usePlayer.getState().nextTrack(); usePlayer.getState().setIndex(ni); }, []); const onPrev = useCallback(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); if (progress > 3) { seek(0); return; } const pi = usePlayer.getState().prevTrack(); usePlayer.getState().setIndex(pi); }, [progress, seek]); const onPlay = useCallback(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); togglePlay(); }, [togglePlay]); if (!track) return null; const cover = coverUrl(track.cover_url); const pct = duration > 0 ? progress / duration : 0; return ( {/* Blurred background art */} {/* Handle */} {/* Header */} Сейчас играет {/* Cover */} {/* Info + like */} {track.title} {track.artist} {/* Progress bar */} seek(v * duration)} /> {fmt(progress)} {fmt(duration)} {/* Controls */} { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); toggleShuffle(); }} active={shuffle}> {playing ? : } { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); cycleRepeat(); }} active={repeat > 0}> ); } function CtlBtn({ onPress, active = false, children }: { onPress: () => void; active?: boolean; children: React.ReactNode; }) { const scale = useSharedValue(1); const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); return ( { scale.value = withSpring(0.82, { damping: 12 }); }} onPressOut={() => { scale.value = withSpring(1, { damping: 12 }); }} hitSlop={12} > {children} ); } function LikeButton() { const scale = useSharedValue(1); const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); return ( { scale.value = withSpring(1.35, { damping: 7 }, () => { scale.value = withSpring(1, { damping: 12 }); }); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }} > ); } function PlayIcon() { return ; } function PauseIcon() { return ; } function PrevIcon() { return ( ); } function NextIcon() { return ( ); } function ShuffleIcon({ active }: { active: boolean }) { const c = active ? Colors.green : Colors.text2; return ( ); } function RepeatIcon({ repeat }: { repeat: 0 | 1 | 2 }) { const c = repeat > 0 ? Colors.green : Colors.text2; return ( {repeat === 2 && ( 1 )} ); } 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({ root: { zIndex: 999 }, dim: { backgroundColor: 'rgba(0,0,0,0.6)' }, sheet: { flex: 1, paddingHorizontal: Spacing.xl }, handleWrap: { alignItems: 'center', paddingBottom: 4 }, handle: { width: 36, height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.3)' }, header: { alignItems: 'center', marginBottom: Spacing.lg }, headerTitle: { color: Colors.text, fontSize: Font.sm, fontWeight: '600', letterSpacing: 0.5 }, coverWrap: { alignItems: 'center', marginBottom: Spacing.xl }, coverShadow: { shadowColor: '#000', shadowOffset: { width: 0, height: 24 }, shadowOpacity: 0.6, shadowRadius: 32 }, cover: { width: COVER_SIZE, height: COVER_SIZE, borderRadius: Radius.xl, backgroundColor: Colors.elevated }, infoRow: { flexDirection: 'row', alignItems: 'center', marginBottom: Spacing.md, gap: Spacing.md }, trackTitle: { color: Colors.text, fontSize: Font.xl, fontWeight: '700', letterSpacing: -0.3 }, artist: { color: 'rgba(255,255,255,0.5)', fontSize: Font.md, marginTop: 4 }, progressWrap: { marginBottom: Spacing.lg }, timeRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: -6 }, timeText: { color: 'rgba(255,255,255,0.35)', fontSize: Font.xs }, controls: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 4 }, ctlBtn: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', borderRadius: Radius.full }, ctlBtnActive: { backgroundColor: 'rgba(255,255,255,0.1)' }, playBtn: { width: 72, height: 72, borderRadius: 36, backgroundColor: Colors.text, alignItems: 'center', justifyContent: 'center', shadowColor: '#fff', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 12 }, repeatBadge: { position: 'absolute', top: -3, right: -3, width: 10, height: 10, borderRadius: 5, backgroundColor: Colors.green, alignItems: 'center', justifyContent: 'center' }, repeatBadgeText: { color: '#000', fontSize: 6, fontWeight: '800' }, });