From d97511476873cf25aceb90f06ce26b6f431d1f49 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 02:59:25 +0200 Subject: [PATCH] =?UTF-8?q?Feat:=20Serverseitige=20Fuzzy-Suche=20f=C3=BCr?= =?UTF-8?q?=20/api/sounds=20und=20Client-Filter=20vereinfacht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/index.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- web/src/App.tsx | 7 ++----- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 01d82dc..a44af00 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -485,7 +485,50 @@ app.get('/api/sounds', (req: Request, res: Response) => { itemsByFolder = allItems.filter((it) => (folderFilter === '' ? it.folder === '' : it.folder === folderFilter)); } } - const filteredItems = itemsByFolder.filter((s) => (q ? s.name.toLowerCase().includes(q) : true)); + // Fuzzy-Score: bevorzugt Präfixe, zusammenhängende Treffer und frühe Positionen + function fuzzyScore(text: string, pattern: string): number { + if (!pattern) return 1; + if (text === pattern) return 2000; + const idx = text.indexOf(pattern); + if (idx !== -1) { + let base = 1000; + if (idx === 0) base += 200; // Präfix-Bonus + return base - idx * 2; // leichte Positionsstrafe + } + // subsequence Matching + let textIndex = 0; + let patIndex = 0; + let score = 0; + let lastMatch = -1; + let gaps = 0; + let firstMatchPos = -1; + while (textIndex < text.length && patIndex < pattern.length) { + if (text[textIndex] === pattern[patIndex]) { + if (firstMatchPos === -1) firstMatchPos = textIndex; + if (lastMatch === textIndex - 1) { + score += 5; // zusammenhängende Treffer belohnen + } + lastMatch = textIndex; + patIndex++; + } else if (firstMatchPos !== -1) { + gaps++; + } + textIndex++; + } + if (patIndex !== pattern.length) return 0; // nicht alle Pattern-Zeichen gefunden + score += Math.max(0, 300 - firstMatchPos * 2); // frühe Starts belohnen + score += Math.max(0, 100 - gaps * 10); // weniger Lücken belohnen + return score; + } + + let filteredItems = itemsByFolder; + if (q) { + const scored = itemsByFolder + .map((it) => ({ it, score: fuzzyScore(it.name.toLowerCase(), q) })) + .filter((x) => x.score > 0) + .sort((a, b) => (b.score - a.score) || a.it.name.localeCompare(b.it.name)); + filteredItems = scored.map((x) => x.it); + } const total = allItems.length; const recentCount = Math.min(10, total); diff --git a/web/src/App.tsx b/web/src/App.tsx index a7a2f90..699c01d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -186,11 +186,8 @@ export default function App() { })(); }, [selected]); - const filtered = useMemo(() => { - const q = query.trim().toLowerCase(); - if (!q) return sounds; - return sounds.filter((s) => s.name.toLowerCase().includes(q)); - }, [sounds, query]); + // Server liefert bereits gefilterte (und ggf. fuzzy-sortierte) Ergebnisse + const filtered = sounds; const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]);