fix(SearchLocal): add text filter functionality and clear filters option

This commit is contained in:
Jaime Idolpx 2026-06-14 22:01:35 -04:00
parent 38cb32be4d
commit 8a2100c587

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { Search, Loader2, RefreshCw, FolderSearch, SlidersHorizontal, ArrowUp, ArrowDown, ArrowUpDown, HardDrive, FolderOpen } from 'lucide-react';
import { Search, Loader2, RefreshCw, FolderSearch, SlidersHorizontal, HardDrive, FolderOpen, X } from 'lucide-react';
import { toast } from 'sonner';
import { humanFileSize, splitPath } from '../webdav';
import type { EntryInfo } from '../webdav';
@ -130,6 +130,7 @@ const _store = {
results: [] as SearchResult[],
hasSearched: false,
showFilter: false,
filterText: '',
filterSystem: null as string | null,
filterVideo: null as string | null,
filterLanguage: null as string | null,
@ -181,6 +182,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
// the user clicks Search the database is almost always ready.
const [dbPhase, setDbPhase] = useState<'idle' | 'ready'>('ready');
const [showFilter, setShowFilter] = useState(() => _store.showFilter);
const [filterText, setFilterText] = useState(() => _store.filterText);
const [filterSystem, setFilterSystem] = useState<string | null>(() => _store.filterSystem);
const [filterVideo, setFilterVideo] = useState<string | null>(() => _store.filterVideo);
const [filterLanguage, setFilterLanguage] = useState<string | null>(() => _store.filterLanguage);
@ -196,12 +198,13 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
_store.results = results;
_store.hasSearched = hasSearched;
_store.showFilter = showFilter;
_store.filterText = filterText;
_store.filterSystem = filterSystem;
_store.filterVideo = filterVideo;
_store.filterLanguage = filterLanguage;
_store.sortField = sortField;
_store.sortDir = sortDir;
}, [query, results, hasSearched, showFilter, filterSystem, filterVideo, filterLanguage, sortField, sortDir]);
}, [query, results, hasSearched, showFilter, filterText, filterSystem, filterVideo, filterLanguage, sortField, sortDir]);
useEffect(() => {
if (_store.scrollTop > 0 && scrollRef.current) {
@ -226,6 +229,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
setIsSearching(true);
setHasSearched(true);
setSearchError(null);
setFilterText('');
setFilterSystem(null);
setFilterVideo(null);
setFilterLanguage(null);
@ -310,10 +314,10 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
return { systems, videos, langs };
}, [results]);
const hasAnyFacets = facets.systems.length > 0 || facets.videos.length > 0 || facets.langs.length > 0;
const visibleResults = useMemo(() => {
const needle = filterText.trim().toLowerCase();
let list = results.filter(r =>
(!needle || r.name.toLowerCase().includes(needle) || r.path.toLowerCase().includes(needle)) &&
(filterSystem === null || r.system === filterSystem) &&
(filterVideo === null || r.video === filterVideo) &&
(filterLanguage === null || r.language === filterLanguage)
@ -330,12 +334,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
else { setSortField(field); setSortDir('asc'); }
};
const SortIcon = ({ field }: { field: SortField }) => {
if (sortField !== field) return <ArrowUpDown className="w-3 h-3 opacity-50" />;
return sortDir === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />;
};
const activeFilters = [filterSystem, filterVideo, filterLanguage].filter(Boolean).length;
const activeFilters = [filterText || null, filterSystem, filterVideo, filterLanguage].filter(Boolean).length;
return (
<>
@ -360,7 +359,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
>
<FolderSearch className="w-4 h-4" />
</button>
{hasAnyFacets && (
{hasSearched && (
<button
onClick={() => setShowFilter(v => !v)}
className={`relative p-1.5 rounded-lg transition-colors ${showFilter ? 'bg-blue-100 text-blue-600' : 'hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600'}`}
@ -400,28 +399,40 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
</button>
</div>
{/* Filter panel */}
<div className={`overflow-hidden transition-all duration-200 ease-in-out ${showFilter && hasAnyFacets ? 'max-h-48 opacity-100 mt-3' : 'max-h-0 opacity-0'}`}>
<div className="space-y-2 pb-1">
</div>
{/* Filter + sort bar — same style as MediaManager */}
<div className={`overflow-hidden flex-shrink-0 transition-all duration-200 ease-in-out ${showFilter ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'}`}>
<div className="bg-neutral-50 border-b border-neutral-200 px-4 py-2 flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="relative flex-1 min-w-0">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-neutral-400 pointer-events-none" />
<input
type="text"
value={filterText}
onChange={e => setFilterText(e.target.value)}
placeholder="Filter…"
className="w-full pl-7 pr-6 py-1 text-sm border border-neutral-300 rounded bg-white"
/>
{filterText && (
<button onClick={() => setFilterText('')} className="absolute right-2 top-1/2 -translate-y-1/2">
<X className="w-3.5 h-3.5 text-neutral-400" />
</button>
)}
</div>
{(['name', 'size'] as SortField[]).map(f => (
<button
key={f}
onClick={() => toggleSort(f)}
className={`text-xs px-2 py-1 rounded border flex-shrink-0 ${sortField === f ? 'border-blue-400 bg-blue-50 text-blue-700' : 'border-neutral-300 bg-white text-neutral-600'}`}
>
{f === 'name' ? 'Name' : 'Size'}{sortField === f ? (sortDir === 'asc' ? ' ↑' : ' ↓') : ''}
</button>
))}
</div>
<FilterChips label="System" values={facets.systems} selected={filterSystem} onSelect={setFilterSystem} />
<FilterChips label="Video" values={facets.videos} selected={filterVideo} onSelect={setFilterVideo} />
<FilterChips label="Language" values={facets.langs} selected={filterLanguage} onSelect={setFilterLanguage} />
<div className="flex items-center gap-1.5 pt-0.5 border-t border-neutral-100">
<span className="text-xs text-neutral-400 font-medium w-14 flex-shrink-0">Sort</span>
<button
onClick={() => toggleSort('name')}
className={`flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium transition-colors ${sortField === 'name' ? 'bg-blue-600 text-white' : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'}`}
>
Name <SortIcon field="name" />
</button>
<button
onClick={() => toggleSort('size')}
className={`flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium transition-colors ${sortField === 'size' ? 'bg-blue-600 text-white' : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'}`}
>
Size <SortIcon field="size" />
</button>
</div>
</div>
</div>
</div>
@ -493,7 +504,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
</p>
{results.length > 0 && (
<button
onClick={() => { setFilterSystem(null); setFilterVideo(null); setFilterLanguage(null); }}
onClick={() => { setFilterText(''); setFilterSystem(null); setFilterVideo(null); setFilterLanguage(null); }}
className="mt-2 text-xs text-blue-600 hover:underline"
>
Clear filters