import SearchInput from '@/components/SearchInput' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { normalizeToDTag } from '@/lib/search-parser' import type { LibraryPublicationRelaySearchAxis } from '@/lib/library-publication-index' import { cn } from '@/lib/utils' import { FileText, Loader2, Search, User, Wifi } from 'lucide-react' import { HTMLAttributes, useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' type LibrarySearchOption = { axis: LibraryPublicationRelaySearchAxis | null search: string input?: string } export default function LibrarySearchBar({ searchQuery, onSearchQueryChange, committedSearch, searchAxis, onCommitSearch, showOnlyMine, onShowOnlyMineChange, mineFilterLoading, onSearchRelays, relaySearchLoading, disabled }: { searchQuery: string onSearchQueryChange: (value: string) => void committedSearch: string searchAxis: LibraryPublicationRelaySearchAxis | null onCommitSearch: (query: string, axis: LibraryPublicationRelaySearchAxis | null) => void showOnlyMine: boolean onShowOnlyMineChange: (value: boolean) => void mineFilterLoading?: boolean onSearchRelays?: () => void relaySearchLoading?: boolean disabled?: boolean }) { const { t } = useTranslation() const [searching, setSearching] = useState(false) const [selectedIndex, setSelectedIndex] = useState(-1) const searchInputRef = useRef(null) const canSearchRelays = searchQuery.trim().length > 0 && !relaySearchLoading const selectableOptions = useMemo((): LibrarySearchOption[] => { const search = searchQuery.trim() if (!search) return [] const normalizedDTag = normalizeToDTag(search) return [ { axis: null, search }, { axis: 'title', search }, { axis: 'author', search }, ...(normalizedDTag ? [{ axis: 'd-tag' as const, search: normalizedDTag, input: search }] : []) ] }, [searchQuery]) const displayList = searching && selectableOptions.length > 0 const blur = () => { setSearching(false) setSelectedIndex(-1) searchInputRef.current?.blur() } const applyOption = useCallback( (option: LibrarySearchOption) => { onCommitSearch(option.input ?? option.search, option.axis) blur() }, [onCommitSearch] ) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.stopPropagation() if (selectableOptions.length <= 0) return applyOption(selectableOptions[selectedIndex >= 0 ? selectedIndex : 0]) return } if (e.key === 'ArrowDown') { e.preventDefault() if (selectableOptions.length <= 0) return setSelectedIndex((prev) => (prev + 1) % selectableOptions.length) return } if (e.key === 'ArrowUp') { e.preventDefault() if (selectableOptions.length <= 0) return setSelectedIndex((prev) => (prev - 1 + selectableOptions.length) % selectableOptions.length) return } if (e.key === 'Escape') { blur() } }, [applyOption, selectableOptions, selectedIndex] ) const list = useMemo(() => { if (selectableOptions.length <= 0) return null return ( <> {selectableOptions.map((option, index) => { if (option.axis === null) { return ( applyOption(option)} /> ) } if (option.axis === 'title') { return ( applyOption(option)} /> ) } if (option.axis === 'author') { return ( applyOption(option)} /> ) } return ( applyOption(option)} /> ) })} ) }, [applyOption, selectableOptions, selectedIndex]) const isCommitted = committedSearch.trim().length > 0 && committedSearch.trim() === searchQuery.trim() const scopeLabel = isCommitted && searchAxis === 'title' ? t('Library search scope title') : isCommitted && searchAxis === 'author' ? t('Library search scope author') : isCommitted && searchAxis === 'd-tag' ? t('Library search scope dtag') : null return (
{displayList && list ? (
e.preventDefault()} >
{list}
) : null} { setSearching(true) setSelectedIndex(-1) onSearchQueryChange(e.target.value) }} onPaste={() => setSearching(true)} onKeyDown={handleKeyDown} onFocus={() => setSearching(true)} onBlur={() => setSearching(false)} placeholder={t('Library search placeholder')} className={cn('bg-surface-background pl-3', displayList && 'z-50')} disabled={disabled} aria-label={t('Library search placeholder')} />
{scopeLabel ? (

{scopeLabel}

) : searchQuery.trim() && !isCommitted ? (

{t('Library search commit hint')}

) : null} {onSearchRelays ? ( ) : null}
{mineFilterLoading ? ( ) : null}
) } function Item({ className, children, selected, ...props }: HTMLAttributes & { selected?: boolean }) { return (
{children}
) } function AllFieldsItem({ search, onClick, selected }: { search: string onClick?: () => void selected?: boolean }) { const { t } = useTranslation() return (
{t('Library search dropdown all')}
{search}
) } function TitleItem({ search, onClick, selected }: { search: string onClick?: () => void selected?: boolean }) { const { t } = useTranslation() return (
{t('Library search dropdown title')}
{search}
) } function AuthorItem({ search, onClick, selected }: { search: string onClick?: () => void selected?: boolean }) { const { t } = useTranslation() return (
{t('Library search dropdown author')}
{search}
) } function DTagItem({ dtag, onClick, selected }: { dtag: string onClick?: () => void selected?: boolean }) { const { t } = useTranslation() return (
{t('Library search dropdown dtag')}
{dtag}
) }