Add files via upload

This commit is contained in:
Savsis 2026-04-28 20:36:28 +05:00 • committed by GitHub
parent 9fb037a648
commit 2a8ba7eeba
14 changed files with 478 additions and 387 deletions

View file

@ -2,12 +2,12 @@ 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 { Image } from 'expo-image';
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;
@ -47,27 +47,30 @@ export default function HomeScreen() {
refreshControl={<RefreshControl refreshing={refresh} onRefresh={onRefresh} tintColor={Colors.text2} />}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<Animated.View entering={FadeIn.duration(400)} style={styles.pageHeader}>
<Text style={styles.pageTitle}>NodeShift Music</Text>
</Animated.View>
{/* Now playing strip (if track exists) */}
<NowPlayingStrip />
{/* Albums */}
{albums.length > 0 && (
<Animated.View entering={FadeInDown.delay(100)} style={styles.section}>
<Text style={styles.secTitle}>Альбомы</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.hRow}>
{albums.map((a, i) => (
<AlbumCard key={a.id} album={a} delay={i * 50} />
<AlbumCard key={a.id} album={a} delay={i * 50} onPress={() => {
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);
}
}} />
))}
</ScrollView>
</Animated.View>
)}
{/* Tracks */}
{tracks.length > 0 && (
<Animated.View entering={FadeInDown.delay(200)} style={styles.section}>
<Text style={styles.secTitle}>Треки</Text>
@ -92,15 +95,21 @@ function NowPlayingStrip() {
const track = queue[index];
if (!track) return null;
const cover = coverUrl(track.cover_url);
return (
<Animated.View entering={FadeInDown.duration(300)} style={styles.npStrip}>
{cover && <Image source={{ uri: cover }} style={StyleSheet.absoluteFill} contentFit="cover" blurRadius={40} />}
{/* Blurred cover background */}
<CoverImage
uri={cover}
title={track.title}
size={160}
radius={0}
blurRadius={40}
style={StyleSheet.absoluteFill as any}
/>
<View style={[StyleSheet.absoluteFill, styles.npDim]} />
<Pressable style={styles.npContent} onPress={() => setExpanded(true)}>
{cover
? <Image source={{ uri: cover }} style={styles.npCover} contentFit="cover" />
: <View style={[styles.npCover, { backgroundColor: Colors.elevated }]} />
}
<CoverImage uri={cover} title={track.title} size={52} radius={Radius.sm} />
<View style={styles.npInfo}>
<Text style={styles.npLabel}>Сейчас играет</Text>
<Text style={styles.npTitle} numberOfLines={1}>{track.title}</Text>
@ -108,7 +117,7 @@ function NowPlayingStrip() {
</View>
<View style={styles.npDot}>
{playing
? <View style={styles.npEq}>{[1,2,3].map(i => <Animated.View key={i} style={[styles.npBar, { height: 4 + i * 4 }]} />)}</View>
? <View style={styles.npEq}>{[1, 2, 3].map(i => <Animated.View key={i} style={[styles.npBar, { height: 4 + i * 4 }]} />)}</View>
: <Svg width={16} height={16} viewBox="0 0 24 24" fill={Colors.text}><Path d="M8 5v14l11-7z" /></Svg>
}
</View>
@ -117,25 +126,23 @@ function NowPlayingStrip() {
);
}
function AlbumCard({ album, delay }: { album: Album; delay: number }) {
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 (
<Animated.View entering={FadeInDown.delay(delay)} style={style}>
<Pressable
onPressIn={() => { scale.value = withSpring(0.95, { damping: 15 }); }}
onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }}
onPress={onPress}
style={styles.albumCard}
>
<Image
source={cover ? { uri: cover } : require('../../assets/placeholder.png')}
style={styles.albumCover}
contentFit="cover"
transition={300}
/>
<CoverImage uri={cover} title={album.title} size={CARD_W} radius={Radius.md} style={{ marginBottom: Spacing.sm }} />
<Text style={styles.albumTitle} numberOfLines={1}>{album.title}</Text>
<Text style={styles.albumArtist} numberOfLines={1}>{album.artist}</Text>
<Text style={styles.albumCount}>{album.track_count} треков</Text>
</Pressable>
</Animated.View>
);
@ -151,12 +158,7 @@ function TrackRow({ track, index, liked, onPress, onLike }: {
return (
<Pressable onPress={onPress} style={[styles.trackRow, active && styles.trackRowActive]}>
<Text style={styles.trackNum}>{index + 1}</Text>
<Image
source={cover ? { uri: cover } : require('../../assets/placeholder.png')}
style={styles.trackCover}
contentFit="cover"
transition={200}
/>
<CoverImage uri={cover} title={track.title} size={44} radius={Radius.sm} />
<View style={styles.trackInfo}>
<Text style={[styles.trackTitle, active && { color: Colors.accent }]} numberOfLines={1}>{track.title}</Text>
<Text style={styles.trackArtist} numberOfLines={1}>{track.artist}</Text>
@ -177,35 +179,33 @@ function fmt(s: number) {
}
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 },
albumCover: { width: CARD_W, height: CARD_W, borderRadius: Radius.md, backgroundColor: Colors.elevated, marginBottom: Spacing.sm },
albumTitle: { color: Colors.text, fontSize: Font.sm, fontWeight: '600' },
albumArtist: { color: Colors.text2, 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' },
trackCover: { width: 44, height: 44, borderRadius: Radius.sm, backgroundColor: Colors.elevated },
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 },
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.55)' },
npContent: { flex: 1, flexDirection: 'row', alignItems: 'center', padding: 14, gap: 12 },
npCover: { width: 52, height: 52, borderRadius: Radius.sm },
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 },
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 },
});