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