import { useCallback } from 'react'; import { View, Text, StyleSheet, Pressable } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, FadeInDown, FadeOutDown, useAnimatedGestureHandler, runOnJS, } from 'react-native-reanimated'; import { PanGestureHandler } from 'react-native-gesture-handler'; import { BlurView } from 'expo-blur'; import { Image } from 'expo-image'; import Svg, { Path } from 'react-native-svg'; import * as Haptics from 'expo-haptics'; import { Colors, Font, Radius, Spacing, TAB_BAR_HEIGHT, MINI_PLAYER_HEIGHT } from '../constants/theme'; import { usePlayer } from '../store'; import { useAudio } from '../hooks/useAudio'; import { coverUrl } from '../services/api'; export default function MiniPlayer() { const { queue, index, playing, expanded, setExpanded } = usePlayer(); const { togglePlay } = useAudio(); const track = queue[index]; const insets = useSafeAreaInsets(); const translateY = useSharedValue(0); const opacity = useSharedValue(1); const gestureHandler = useAnimatedGestureHandler({ onActive: (e) => { if (e.translationY < 0) { translateY.value = Math.max(e.translationY, -80); opacity.value = 1 + e.translationY / 80; } }, onEnd: (e) => { if (e.translationY < -40 || e.velocityY < -400) { runOnJS(openPlayer)(); } translateY.value = withSpring(0); opacity.value = withSpring(1); }, }); const animStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }], opacity: opacity.value, })); const openPlayer = useCallback(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setExpanded(true); }, [setExpanded]); const onPlayPress = useCallback(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); togglePlay(); }, [togglePlay]); if (!track || expanded) return null; const cover = coverUrl(track.cover_url); return ( {/* Cover */} {/* Info */} {track.title} {track.artist} {/* Play/Pause */} {playing ? : } {/* Next */} { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); const next = usePlayer.getState().nextTrack(); usePlayer.getState().setIndex(next); }} hitSlop={12} > {/* Progress line */} ); } function ProgressLine() { const { progress, duration } = usePlayer(); const pct = duration > 0 ? (progress / duration) * 100 : 0; return ( ); } function PlayIcon() { return ; } function PauseIcon() { return ; } function NextIcon() { return ; } const styles = StyleSheet.create({ wrap: { position: 'absolute', left: 0, right: 0, height: MINI_PLAYER_HEIGHT, overflow: 'hidden' }, border: { position: 'absolute', top: 0, left: 0, right: 0, height: StyleSheet.hairlineWidth, backgroundColor: Colors.border }, row: { flex: 1, flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.md, gap: Spacing.md }, cover: { width: 42, height: 42, borderRadius: Radius.sm, backgroundColor: Colors.elevated }, info: { flex: 1 }, title: { color: Colors.text, fontSize: Font.sm, fontWeight: '600' }, artist: { color: Colors.text2, fontSize: Font.xs, marginTop: 2 }, btn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' }, progressBg: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, backgroundColor: Colors.border }, progressFill:{ height: 2, backgroundColor: Colors.green }, });