Add files via upload
This commit is contained in:
parent
81ce3b2271
commit
9fb037a648
13 changed files with 832 additions and 518 deletions
|
|
@ -1,13 +1,13 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
View, Text, TextInput, FlatList, StyleSheet, Pressable, Keyboard,
|
||||
} from 'react-native';
|
||||
import Animated, { FadeIn, FadeInDown, useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||
import { View, Text, TextInput, FlatList, StyleSheet, Pressable, Keyboard } from 'react-native';
|
||||
import Animated, { FadeIn, FadeInDown } 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 { searchTracks, Track, coverUrl } from '../../services/api';
|
||||
import { usePlayer } from '../../store';
|
||||
import { usePlayer, useLikes } from '../../store';
|
||||
|
||||
export default function SearchScreen() {
|
||||
const [query, setQuery] = useState('');
|
||||
|
|
@ -16,6 +16,7 @@ export default function SearchScreen() {
|
|||
const [searched, setSearched] = useState(false);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { setQueue } = usePlayer();
|
||||
const { toggleLike, isLiked } = useLikes();
|
||||
|
||||
const doSearch = useCallback(async (q: string) => {
|
||||
if (!q.trim()) { setResults([]); setSearched(false); return; }
|
||||
|
|
@ -28,6 +29,7 @@ export default function SearchScreen() {
|
|||
|
||||
const playTrack = (index: number) => {
|
||||
Keyboard.dismiss();
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
setQueue(results, index);
|
||||
usePlayer.getState().setExpanded(true);
|
||||
};
|
||||
|
|
@ -52,14 +54,12 @@ export default function SearchScreen() {
|
|||
|
||||
{!searched && (
|
||||
<Animated.View entering={FadeIn} style={styles.emptyWrap}>
|
||||
<Text style={styles.emptyIcon}>🎵</Text>
|
||||
<Text style={styles.emptyText}>Введите название трека{'\n'}или имя исполнителя</Text>
|
||||
<Text style={styles.emptyText}>Введите название трека или исполнителя</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{searched && !loading && results.length === 0 && (
|
||||
<Animated.View entering={FadeIn} style={styles.emptyWrap}>
|
||||
<Text style={styles.emptyIcon}>🔍</Text>
|
||||
<Text style={styles.emptyText}>Ничего не найдено</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
|
@ -68,69 +68,63 @@ export default function SearchScreen() {
|
|||
data={results}
|
||||
keyExtractor={t => String(t.id)}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: Spacing.md,
|
||||
paddingBottom: TAB_BAR_HEIGHT + MINI_PLAYER_HEIGHT + 24,
|
||||
}}
|
||||
contentContainerStyle={{ paddingHorizontal: Spacing.md, paddingBottom: TAB_BAR_HEIGHT + MINI_PLAYER_HEIGHT + 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResult track={item} index={index} onPress={() => playTrack(index)} />
|
||||
<Animated.View entering={FadeInDown.delay(index * 30).duration(250)}>
|
||||
<Pressable
|
||||
onPress={() => playTrack(index)}
|
||||
style={[styles.row, usePlayer.getState().queue[usePlayer.getState().index]?.id === item.id && styles.rowActive]}
|
||||
>
|
||||
{(() => {
|
||||
const cover = coverUrl(item.cover_url);
|
||||
const liked = isLiked(item.id);
|
||||
const active = usePlayer.getState().queue[usePlayer.getState().index]?.id === item.id;
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
source={cover ? { uri: cover } : require('../../assets/placeholder.png')}
|
||||
style={styles.cover}
|
||||
contentFit="cover"
|
||||
transition={200}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.trackTitle, active && { color: Colors.accent }]} numberOfLines={1}>{item.title}</Text>
|
||||
<Text style={styles.artist} numberOfLines={1}>{item.artist}</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => toggleLike(item.id)} hitSlop={12}>
|
||||
<Svg width={16} height={16} viewBox="0 0 24 24" fill={liked ? Colors.pink : 'none'}>
|
||||
<Path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" stroke={liked ? Colors.pink : Colors.text3} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</Svg>
|
||||
</Pressable>
|
||||
<Text style={styles.dur}>{fmt(item.duration)}</Text>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResult({ track, index, onPress }: { track: Track; index: number; onPress: () => void }) {
|
||||
const { queue, index: qi } = usePlayer();
|
||||
const active = queue[qi]?.id === track.id;
|
||||
const scale = useSharedValue(1);
|
||||
const aStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
|
||||
const cover = coverUrl(track.cover_url);
|
||||
|
||||
return (
|
||||
<Animated.View entering={FadeInDown.delay(index * 40).duration(250)} style={aStyle}>
|
||||
<Pressable
|
||||
onPressIn={() => { scale.value = withSpring(0.97, { damping: 15 }); }}
|
||||
onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }}
|
||||
onPress={onPress}
|
||||
style={[styles.row, active && styles.rowActive]}
|
||||
>
|
||||
<Image
|
||||
source={cover ? { uri: cover } : require('../../assets/placeholder.png')}
|
||||
style={styles.cover}
|
||||
contentFit="cover"
|
||||
transition={200}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.trackTitle, active && { color: Colors.green }]} numberOfLines={1}>
|
||||
{track.title}
|
||||
</Text>
|
||||
<Text style={styles.artist} numberOfLines={1}>{track.artist}</Text>
|
||||
</View>
|
||||
<Text style={styles.dur}>{fmt(track.duration)}</Text>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
container: { flex: 1, backgroundColor: Colors.bg },
|
||||
title: { color: Colors.text, fontSize: Font.xxl, fontWeight: '800', letterSpacing: -0.5, paddingHorizontal: Spacing.md, paddingTop: Spacing.md, marginBottom: Spacing.md },
|
||||
inputWrap: { marginHorizontal: Spacing.md, marginBottom: Spacing.md, backgroundColor: Colors.surface, borderRadius: Radius.md, paddingHorizontal: Spacing.md },
|
||||
input: { color: Colors.text, fontSize: Font.md, paddingVertical: 13 },
|
||||
emptyWrap: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingBottom: 120 },
|
||||
emptyIcon: { fontSize: 48, marginBottom: Spacing.md },
|
||||
emptyText: { color: Colors.text2, fontSize: Font.md, textAlign: 'center', lineHeight: 24 },
|
||||
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, gap: Spacing.md, borderRadius: Radius.sm },
|
||||
rowActive: { backgroundColor: Colors.surface + '80' },
|
||||
cover: { width: 48, height: 48, borderRadius: Radius.sm, backgroundColor: Colors.elevated },
|
||||
trackTitle:{ color: Colors.text, fontSize: Font.md, fontWeight: '600' },
|
||||
artist: { color: Colors.text2, fontSize: Font.sm, marginTop: 2 },
|
||||
dur: { color: Colors.text3, fontSize: Font.xs },
|
||||
container: { flex: 1, backgroundColor: Colors.bg },
|
||||
title: { color: Colors.text, fontSize: Font.xxl, fontWeight: '800', letterSpacing: -0.5, paddingHorizontal: Spacing.md, paddingTop: Spacing.md, marginBottom: Spacing.md },
|
||||
inputWrap: { marginHorizontal: Spacing.md, marginBottom: Spacing.md, backgroundColor: Colors.surface, borderRadius: Radius.md, paddingHorizontal: Spacing.md },
|
||||
input: { color: Colors.text, fontSize: Font.md, paddingVertical: 12 },
|
||||
emptyWrap: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingBottom: 120 },
|
||||
emptyText: { color: Colors.text3, fontSize: Font.sm, textAlign: 'center' },
|
||||
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, gap: Spacing.md, borderRadius: Radius.sm },
|
||||
rowActive: { backgroundColor: Colors.surface },
|
||||
cover: { width: 48, height: 48, borderRadius: Radius.sm, backgroundColor: Colors.elevated },
|
||||
trackTitle: { color: Colors.text, fontSize: Font.md, fontWeight: '600' },
|
||||
artist: { color: Colors.text2, fontSize: Font.sm, marginTop: 2 },
|
||||
dur: { color: Colors.text3, fontSize: Font.xs },
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue