import React, { useEffect, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { Play, SlidersHorizontal } from 'lucide-react'; import { search, getGenres, getAlbumsByGenre, getAlbumList, getRandomSongs, SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong, } from '../api/subsonic'; import { usePlayerStore, songToTrack } from '../store/playerStore'; import { useTranslation } from 'react-i18next'; import AlbumRow from '../components/AlbumRow'; import ArtistRow from '../components/ArtistRow'; import CustomSelect from '../components/CustomSelect'; import { useDragDrop } from '../contexts/DragDropContext'; type ResultType = 'all' | 'artists' | 'albums' | 'songs'; interface SearchOpts { query: string; genre: string; yearFrom: string; yearTo: string; resultType: ResultType; } interface Results { artists: SubsonicArtist[]; albums: SubsonicAlbum[]; songs: SubsonicSong[]; } export default function AdvancedSearch() { const { t } = useTranslation(); const [params] = useSearchParams(); const navigate = useNavigate(); const psyDrag = useDragDrop(); const playTrack = usePlayerStore(s => s.playTrack); const openContextMenu = usePlayerStore(s => s.openContextMenu); const [query, setQuery] = useState(params.get('q') ?? ''); const [genre, setGenre] = useState(''); const [yearFrom, setYearFrom] = useState(''); const [yearTo, setYearTo] = useState(''); const [resultType, setResultType] = useState('all'); const [genres, setGenres] = useState([]); const [results, setResults] = useState(null); const [loading, setLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); const [genreNote, setGenreNote] = useState(false); const runSearch = async (opts: SearchOpts) => { setLoading(true); setHasSearched(true); setGenreNote(false); const { query: q, genre: g, yearFrom: yf, yearTo: yt, resultType: rt } = opts; const from = yf ? parseInt(yf) : null; const to = yt ? parseInt(yt) : null; let artists: SubsonicArtist[] = []; let albums: SubsonicAlbum[] = []; let songs: SubsonicSong[] = []; try { if (q.trim()) { const r = await search(q.trim(), { artistCount: 30, albumCount: 50, songCount: 100 }); artists = r.artists; albums = r.albums; songs = r.songs; if (g) { albums = albums.filter(a => a.genre?.toLowerCase() === g.toLowerCase()); songs = songs.filter(s => s.genre?.toLowerCase() === g.toLowerCase()); } if (from !== null) { albums = albums.filter(a => !a.year || a.year >= from); songs = songs.filter(s => !s.year || s.year >= from); } if (to !== null) { albums = albums.filter(a => !a.year || a.year <= to); songs = songs.filter(s => !s.year || s.year <= to); } } else if (g) { const [albumRes, songRes] = await Promise.all([ rt === 'songs' || rt === 'artists' ? Promise.resolve([]) : getAlbumsByGenre(g, 50), rt === 'albums' || rt === 'artists' ? Promise.resolve([]) : getRandomSongs(100, g), ]); albums = albumRes as SubsonicAlbum[]; songs = songRes as SubsonicSong[]; if (from !== null) albums = albums.filter(a => !a.year || a.year >= from); if (to !== null) albums = albums.filter(a => !a.year || a.year <= to); if (songs.length > 0) setGenreNote(true); } else if (from !== null || to !== null) { const fromYear = from ?? 1900; const toYear = to ?? new Date().getFullYear(); albums = await getAlbumList('byYear', 100, 0, { fromYear, toYear }); } setResults({ artists: rt === 'albums' || rt === 'songs' ? [] : artists, albums: rt === 'artists' || rt === 'songs' ? [] : albums, songs: rt === 'artists' || rt === 'albums' ? [] : songs, }); } catch { setResults({ artists: [], albums: [], songs: [] }); } setLoading(false); }; useEffect(() => { getGenres().then(data => setGenres(data.sort((a, b) => a.value.localeCompare(b.value))) ).catch(() => {}); const q = params.get('q') ?? ''; if (q) runSearch({ query: q, genre: '', yearFrom: '', yearTo: '', resultType: 'all' }); }, []); const handleSubmit = (e?: React.FormEvent) => { e?.preventDefault(); runSearch({ query, genre, yearFrom, yearTo, resultType }); }; const total = results ? results.artists.length + results.albums.length + results.songs.length : 0; const typeOptions: { id: ResultType; label: string }[] = [ { id: 'all', label: t('search.advancedAll') }, { id: 'artists', label: t('search.artists') }, { id: 'albums', label: t('search.albums') }, { id: 'songs', label: t('search.songs') }, ]; const genreSelectOptions = [ { value: '', label: t('search.advancedAllGenres') }, ...genres.map(g => ({ value: g.value, label: g.value })), ]; return (

{t('search.advanced')}

{/* ── Filter panel ──────────────────────────────────────── */}
{/* Row 1: Search term */}
{t('search.advancedSearchTerm')} setQuery(e.target.value)} placeholder={t('search.advancedSearchPlaceholder')} style={{ flex: 1 }} autoFocus />
{/* Row 2: Genre + Year */}
{t('search.advancedGenre')}
{t('search.advancedYear')} setYearFrom(e.target.value)} placeholder={t('search.advancedYearFrom')} style={{ width: 96 }} /> setYearTo(e.target.value)} placeholder={t('search.advancedYearTo')} style={{ width: 96 }} />
{/* Row 3: Result type + Search button */}
{typeOptions.map(opt => ( ))}
{/* ── Results ───────────────────────────────────────────── */} {!hasSearched ? (
{t('search.advancedEmpty')}
) : loading ? (
) : total === 0 ? (
{t('search.advancedNoResults')}
) : (
{results && results.artists.length > 0 && ( )} {results && results.albums.length > 0 && ( )} {results && results.songs.length > 0 && (

{t('search.songs')} ({results.songs.length}) {genreNote && ( — {t('search.advancedGenreNote')} )}

{t('randomMix.trackTitle')} {t('randomMix.trackArtist')} {t('randomMix.trackAlbum')} {t('randomMix.trackGenre')} {t('randomMix.trackDuration')}
{results.songs.map(song => { const track = songToTrack(song); return (
playTrack(track, results.songs.map(songToTrack))} role="row" onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'song'); }} onMouseDown={e => { if (e.button !== 0) return; e.preventDefault(); const sx = e.clientX, sy = e.clientY; const onMove = (me: MouseEvent) => { if (Math.abs(me.clientX - sx) > 5 || Math.abs(me.clientY - sy) > 5) { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); psyDrag.startDrag({ data: JSON.stringify({ type: 'song', track }), label: song.title }, me.clientX, me.clientY); } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }} >
{song.title}
song.artistId && navigate(`/artist/${song.artistId}`)} > {song.artist}
navigate(`/album/${song.albumId}`)} > {song.album}
{song.genre ?? '—'}
{Math.floor(song.duration / 60)}:{(song.duration % 60).toString().padStart(2, '0')}
); })}
)}
)}
); }