import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl'; import { search } from '../api/subsonicSearch'; import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { X, Search, Disc3, Users, Music, Music2, Clock, ChevronRight } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; import { useAuthStore } from '../store/authStore'; import { useTranslation } from 'react-i18next'; import CachedImage, { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from './CachedImage'; import { showToast } from '../utils/ui/toast'; const STORAGE_KEY = 'psysonic_recent_searches'; const MAX_RECENT = 6; function loadRecent(): string[] { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; } } function saveRecent(q: string, prev: string[]): string[] { const updated = [q.trim(), ...prev.filter(s => s !== q.trim())].slice(0, MAX_RECENT); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); return updated; } function debounce(fn: (q: string) => void, ms: number): (q: string) => void { let timer: ReturnType; return (q: string) => { clearTimeout(timer); timer = setTimeout(() => fn(q), ms); }; } function MobileSearchArtistThumb({ artist }: { artist: Pick }) { const [failed, setFailed] = useState(false); const coverId = artist.coverArt || artist.id; const src = useMemo(() => buildCoverArtUrl(coverId, 80), [coverId]); const ck = useMemo(() => coverArtCacheKey(coverId, 80), [coverId]); useEffect(() => { setFailed(false); }, [coverId]); if (failed) { return (
); } return ( setFailed(true)} /> ); } export default function MobileSearchOverlay({ onClose }: { onClose: () => void }) { const { t } = useTranslation(); const navigate = useNavigate(); const enqueue = usePlayerStore(s => s.enqueue); const [query, setQuery] = useState(''); const [results, setResults] = useState(null); const [loading, setLoading] = useState(false); const [recentSearches, setRecentSearches] = useState(loadRecent); const inputRef = useRef(null); const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion); useEffect(() => { inputRef.current?.focus(); }, []); useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, []); const doSearch = useCallback( debounce(async (q: string) => { if (!q.trim()) { setResults(null); setLoading(false); return; } setLoading(true); try { setResults(await search(q)); } finally { setLoading(false); } }, 300), [musicLibraryFilterVersion] ); useEffect(() => { doSearch(query); }, [query, doSearch]); const commit = (q: string) => { if (q.trim()) setRecentSearches(prev => saveRecent(q, prev)); }; const goTo = (path: string) => { commit(query); navigate(path); onClose(); }; const goCategory = (path: string) => { navigate(path); onClose(); }; const enqueueSong = (song: SearchResults['songs'][number]) => { commit(query); const track = songToTrack(song); enqueue([track]); showToast(t('search.addedToQueueToast', { title: track.title }), 2200, 'info'); onClose(); }; const useRecent = (term: string) => { setQuery(term); inputRef.current?.focus(); }; const removeRecent = (term: string, e: React.MouseEvent) => { e.stopPropagation(); setRecentSearches(prev => { const updated = prev.filter(s => s !== term); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); return updated; }); }; const hasResults = results && (results.artists.length || results.albums.length || results.songs.length); const showEmpty = !query; return createPortal(
{/* ── Search bar ── */}
{loading ? (
) : ( )} setQuery(e.target.value)} autoComplete="off" autoCorrect="off" autoCapitalize="off" /> {query && ( )}
{/* ── Empty state ── */} {showEmpty && (
{recentSearches.length > 0 && (
{t('search.recentSearches')}
{recentSearches.map(term => ( ))}
)}
{t('search.browse')}
{t('search.emptyHint')}
)} {/* ── No results ── */} {!loading && query && !hasResults && (
{t('search.noResults', { query })}
)} {/* ── Results ── */} {hasResults && ( <> {results!.artists.length > 0 && (
{t('search.artists')}
{results!.artists.map(a => ( ))}
)} {results!.albums.length > 0 && (
{t('search.albums')}
{results!.albums.map(a => ( ))}
)} {results!.songs.length > 0 && (
{t('search.songs')}
{results!.songs.map(s => ( ))}
)} )}
, document.body ); }