import { useState, useCallback } from 'react'; import { View, Text, StyleSheet, Pressable, Alert } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSpring, } from 'react-native-reanimated'; import Svg, { Path, Circle } from 'react-native-svg'; import * as Haptics from 'expo-haptics'; import * as FileSystem from 'expo-file-system'; import { Colors, Radius } from '../constants/theme'; import { useOffline } from '../store'; import { Track, coverUrl } from '../services/api'; interface Props { track: Track; size?: number; showLabel?: boolean; } const DL_DIR = FileSystem.documentDirectory + 'tracks/'; async function ensureDir() { const info = await FileSystem.getInfoAsync(DL_DIR); if (!info.exists) await FileSystem.makeDirectoryAsync(DL_DIR, { intermediates: true }); } function getLocalPath(trackId: number, filename: string) { const ext = filename.split('.').pop() || 'mp3'; return `${DL_DIR}${trackId}.${ext}`; } export default function TrackDownloadButton({ track, size = 22, showLabel = false }: Props) { const { downloadedTracks, downloading, setDownloading, addDownloadedTrack } = useOffline(); const isDownloaded = downloadedTracks.has(track.id); const dlProgress = downloading.get(track.id); const isDownloading = dlProgress !== undefined; const scale = useSharedValue(1); const scaleStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); const progressWidth = useAnimatedStyle(() => ({ width: withTiming(`${(dlProgress ?? 0) * 100}%` as any, { duration: 150 }), })); const startDownload = useCallback(async () => { if (isDownloaded || isDownloading) return; Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); scale.value = withSpring(0.85, { damping: 8 }, () => { scale.value = withSpring(1); }); try { await ensureDir(); const url = track.cdn_url || track.cdn_url2; if (!url) { Alert.alert('Ошибка', 'URL трека недоступен'); return; } const localPath = getLocalPath(track.id, track.filename || `${track.id}.mp3`); setDownloading(track.id, 0.01); const callback = (dp: FileSystem.DownloadProgressData) => { const pct = dp.totalBytesExpectedToWrite > 0 ? dp.totalBytesWritten / dp.totalBytesExpectedToWrite : 0; setDownloading(track.id, pct); }; const downloadResumable = FileSystem.createDownloadResumable(url, localPath, {}, callback); const result = await downloadResumable.downloadAsync(); if (result?.uri) { addDownloadedTrack(track.id); setDownloading(track.id, 1); // Save metadata locally const metaPath = `${DL_DIR}${track.id}.json`; await FileSystem.writeAsStringAsync(metaPath, JSON.stringify({ id: track.id, title: track.title, artist: track.artist, filename: track.filename, cover_url: track.cover_url, localPath: result.uri, })); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } } catch (e: any) { setDownloading(track.id, 1); Alert.alert('Ошибка загрузки', e?.message || 'Попробуйте снова'); } }, [track, isDownloaded, isDownloading]); const iconColor = isDownloaded ? Colors.accent : isDownloading ? 'rgba(255,255,255,0.4)' : 'rgba(255,255,255,0.7)'; return ( {isDownloading ? ( {Math.round((dlProgress ?? 0) * 100)}% ) : ( {isDownloaded ? ( ) : ( )} {showLabel && ( {isDownloaded ? 'Скачано' : 'Скачать'} )} )} ); } const s = StyleSheet.create({ btn: { alignItems: 'center', justifyContent: 'center' }, progressWrap: { alignItems: 'center', gap: 4 }, progressBg: { width: 48, height: 3, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.12)', overflow: 'hidden' }, progressFill: { height: 3, backgroundColor: Colors.accent, borderRadius: 2 }, pctText: { color: Colors.accent, fontSize: 9, fontWeight: '700' }, label: { fontSize: 12, fontWeight: '600' }, });