import { SearchIcon, SearchClearIcon } from './SvgIcons' import { useState, useRef, useEffect, ChangeEvent } from 'react' import { useJellyfinContext } from '../context/JellyfinContext/JellyfinContext' import { MediaItem } from '../api/jellyfin' import { InlineLoader } from './InlineLoader' import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client' import { Link, useNavigate } from 'react-router-dom' import { useDropdownContext } from '../context/DropdownContext/DropdownContext' import { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext' import { AlbumIcon, ArtistsIcon, PlaylistIcon, TrackIcon } from './SvgIcons' import { BookmarkFillIcon } from '@primer/octicons-react' import '../components/Sidenav.css' export const SearchBar = () => { const api = useJellyfinContext() const playback = usePlaybackContext() const searchInputRef = useRef(null) const navigate = useNavigate() const [isExpanded, setIsExpanded] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState([]) const [searchLoading, setSearchLoading] = useState(false) const [searchError, setSearchError] = useState(null) const [searchAttempted, setSearchAttempted] = useState(false) const dropdown = useDropdownContext() useEffect(() => { const debounceTimer = setTimeout(() => { const fetchSearchResults = async () => { if (!searchQuery || !api.auth.serverUrl || !api.auth.token || !api.auth.userId) { setSearchResults([]) setSearchAttempted(false) return } setSearchLoading(true) setSearchError(null) setSearchAttempted(true) try { // Fetch artists from /Artists endpoint const [artistResponse, itemsResponse, genreResponse] = await Promise.all([ api.searchArtists(searchQuery, 20), api.searchItems(searchQuery, 40), api.searchGenres(searchQuery, 20), ]) // Fetch songs, albums, and playlists from /Items endpoint const artists = artistResponse.slice(0, 4) const songs = itemsResponse.filter(item => item.Type === 'Audio').slice(0, 6) const albums = itemsResponse.filter(item => item.Type === 'MusicAlbum').slice(0, 4) const playlists = itemsResponse.filter(item => item.Type === 'Playlist').slice(0, 4) const genres = genreResponse.slice(0, 4) const limitedResults = [...songs, ...artists, ...albums, ...playlists, ...genres] setSearchResults(limitedResults) } catch (err) { console.error('Search Error:', err) setSearchError('Failed to load search results') } finally { setSearchLoading(false) } } fetchSearchResults() }, 200) return () => clearTimeout(debounceTimer) }, [searchQuery, api.auth.serverUrl, api.auth.token, api.auth.userId, api]) const handleSearchChange = (e: ChangeEvent) => { setSearchQuery(e.target.value) } const handleClearSearch = () => { setSearchQuery('') setSearchResults([]) setSearchAttempted(false) if (searchInputRef.current) { searchInputRef.current.focus() } } const handleSongClick = (song: MediaItem) => { if (song) { if (song.Id === playback.currentTrack?.Id) { playback.togglePlayPause() } else { playback.setCurrentPlaylistSimple({ playlist: [song], title: 'Search Result' }) playback.playTrack(0) } } } const handleExpand = () => { setIsExpanded(true) } const handleCollapse = () => { if (!searchQuery) { setIsExpanded(false) setSearchResults([]) setSearchAttempted(false) } } const handleSeeMore = () => { if (searchQuery) { navigate(`/search/${encodeURIComponent(searchQuery)}`) setIsExpanded(false) setSearchQuery('') setSearchResults([]) setSearchAttempted(false) } } return (
e.target.classList.add('focused')} onBlur={e => { e.target.classList.remove('focused') handleCollapse() }} className={isExpanded ? 'expanded' : ''} /> {!searchLoading && searchQuery && (
)} {searchLoading && (
)}
{isExpanded && searchQuery && (
{searchError &&
{searchError}
} {!searchLoading && searchAttempted && !searchError && searchResults.length === 0 && (
Search for '{searchQuery}' yields no results
)} {!searchLoading && !searchError && searchResults.length > 0 && (
{searchResults.map(item => { const isActive = dropdown.selectedItem?.Id === item.Id && dropdown.isOpen const itemClass = [ item.Type === 'Audio' && playback.currentTrack?.Id === item.Id ? playback.isPlaying ? 'playing' : 'paused' : '', isActive ? 'active' : '', ] .filter(Boolean) .join(' ') return item.Type === 'Audio' ? (
handleSongClick(item)} className={`result ${itemClass}`} onContextMenu={e => dropdown.onContextMenu(e, { item })} onTouchStart={e => dropdown.onTouchStart(e, { item })} onTouchMove={dropdown.onTouchClear} onTouchEnd={dropdown.onTouchClear} >
{item.Name}{' '} ( {item.ArtistItems?.[0]?.Name || item.Artists?.[0] || 'Unknown Artist'} )
) : ( dropdown.onContextMenu(e, { item: item }), onTouchStart: e => dropdown.onTouchStart(e, { item }), onTouchMove: dropdown.onTouchClear, onTouchEnd: dropdown.onTouchClear, })} onClick={() => setIsExpanded(false)} > {item.Type === BaseItemKind.MusicArtist && (
{item.Name}
)} {item.Type === BaseItemKind.MusicAlbum && (
{item.Name}
)} {item.Type === BaseItemKind.Playlist && (
{item.Name}
)} {item.Type === BaseItemKind.Genre && (
{item.Name}
)} ) })}
See more results
)}
)}
) }