import { unwrap } from '@pipu/core'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { useRouter } from 'expo-router'; import * as SecureStore from 'expo-secure-store'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FlatList, Pressable, Text, TextInput, View } from 'react-native'; import { useSession } from '../../lib/session'; import { player } from '../../player/controller'; import { usePlayerState } from '../../player/hooks'; import { radius, useColors } from '../../theme'; import { Cover, Icon, Screen, TrackRow, useErrorText } from '../../ui/kit'; const RECENT_KEY = 'pipu.recent_searches'; /** Поиск: подсказки по мере ввода (debounce 250 мс), недавние запросы — локально. */ export default function SearchScreen() { const colors = useColors(); const { t } = useTranslation(); const session = useSession(); const router = useRouter(); const current = usePlayerState().track; const errorText = useErrorText(); const [q, setQ] = useState(''); const [debounced, setDebounced] = useState(''); const [recent, setRecent] = useState([]); useEffect(() => { void SecureStore.getItemAsync(RECENT_KEY).then((v) => setRecent(v ? (JSON.parse(v) as string[]) : [])); }, []); useEffect(() => { const id = setTimeout(() => setDebounced(q.trim()), 250); return () => clearTimeout(id); }, [q]); const remember = (query: string) => { const next = [query, ...recent.filter((r) => r !== query)].slice(0, 10); setRecent(next); void SecureStore.setItemAsync(RECENT_KEY, JSON.stringify(next)); }; const res = useQuery({ queryKey: ['m-search', debounced], enabled: debounced !== '', placeholderData: keepPreviousData, queryFn: async ({ signal }) => unwrap(await session.client.GET('/api/v1/search', { params: { query: { q: debounced } }, signal })), }); const data = debounced ? res.data : undefined; return ( q.trim() && remember(q.trim())} placeholder={t('search.placeholder')} placeholderTextColor={colors.text3} autoCorrect={false} autoCapitalize="none" accessibilityLabel={t('search.label')} style={{ flex: 1, color: colors.text, fontSize: 16 }} returnKeyType="search" /> {!!q && ( setQ('')} hitSlop={10} accessibilityRole="button" accessibilityLabel={t('form.close')}> )} {data?.corrected_query && {t('search.corrected', { query: data.corrected_query })}} {res.error && {errorText(res.error)}} {!debounced ? ( {recent.length > 0 && ( {t('mobile.recent_searches')} { setRecent([]); void SecureStore.deleteItemAsync(RECENT_KEY); }} accessibilityRole="button"> {t('mobile.clear_recent')} )} {recent.map((r) => ( setQ(r)} style={{ flexDirection: 'row', alignItems: 'center', gap: 10, paddingVertical: 10 }}> {r} ))} ) : ( tr.id} ListHeaderComponent={ {data?.artists.map((a) => ( { remember(debounced); router.push(`/artist/${a.id}`); }} style={{ flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 8 }}> {a.name} {t('search.artist_kind')} ))} {data?.albums.map((al) => ( { remember(debounced); router.push(`/album/${al.id}`); }} style={{ flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 8 }}> {al.title} {[al.artist.name, al.year].filter(Boolean).join(' · ')} ))} } ListEmptyComponent={ res.isSuccess && !data?.artists.length && !data?.albums.length ? ( {t('search.nothing')} ) : null } renderItem={({ item, index }) => ( { remember(debounced); void player.playList(data?.tracks ?? [], index, { type: 'search', id: debounced }); }} onLike={() => void player.react(item, item.reaction === 'like' ? null : 'like').then(() => res.refetch())} /> )} /> )} ); }