fix(SearchLocal): add text filter functionality and clear filters option
This commit is contained in:
parent
38cb32be4d
commit
8a2100c587
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user