import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { AhoCorasick } from '@/lib/aho'; import { ListSystemsWithRegions, SetDestinationForAll, ListCharacters, StartESILogin } from 'wailsjs/go/main/App'; import { toast } from '@/hooks/use-toast'; interface SearchResult { system: string; region: string; } async function loadAllSystems(): Promise> { const list = await ListSystemsWithRegions(); const seen = new Set(); const out: Array = []; for (const item of list) { const system = String(item.system); if (seen.has(system)) continue; seen.add(system); out.push({ system, region: String(item.region) }); } return out; } export const SearchDialog: React.FC = () => { const navigate = useNavigate(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [all, setAll] = useState>([]); const [results, setResults] = useState>([]); const inputRef = useRef(null); const automaton = useMemo(() => { const ac = new AhoCorasick(); if (query.trim().length > 0) ac.add(query.toLowerCase()); ac.build(); return ac; }, [query]); useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { const isCtrlF = (e.key === 'f' || e.key === 'F') && (e.ctrlKey || e.metaKey); if (isCtrlF) { e.preventDefault(); setOpen(true); setTimeout(() => inputRef.current?.focus(), 0); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, []); useEffect(() => { if (!open) return; if (all.length === 0) { loadAllSystems().then(setAll); } }, [open, all.length]); useEffect(() => { const q = query.trim().toLowerCase(); if (q.length === 0) { setResults([]); return; } const scored: Array<{ s: SearchResult; idx: number }> = []; for (const r of all) { const idx = automaton.searchFirstIndex(r.system.toLowerCase()); if (idx >= 0) scored.push({ s: r, idx }); } scored.sort((a, b) => { if (a.idx !== b.idx) return a.idx - b.idx; // earlier index first if (a.s.system.length !== b.s.system.length) return a.s.system.length - b.s.system.length; // shorter name next return a.s.system.localeCompare(b.s.system); }); setResults(scored.slice(0, 10).map(x => x.s)); }, [query, all, automaton]); const onSelect = (r: SearchResult) => { setOpen(false); setQuery(''); navigate(`/regions/${encodeURIComponent(r.region)}?focus=${encodeURIComponent(r.system)}`); }; const ensureAnyLoggedIn = async (): Promise => { try { const list = await ListCharacters(); if (Array.isArray(list) && list.length > 0) return true; await StartESILogin(); toast({ title: 'EVE Login', description: 'Complete login in your browser, then retry.' }); return false; } catch (e: any) { await StartESILogin(); toast({ title: 'EVE Login', description: 'Complete login in your browser, then retry.', variant: 'destructive' }); return false; } }; const handleResultClick = async (e: React.MouseEvent, r: SearchResult) => { if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); try { if (!(await ensureAnyLoggedIn())) return; await SetDestinationForAll(r.system, true, false); toast({ title: 'Destination set', description: r.system }); } catch (err: any) { toast({ title: 'Failed to set destination', description: String(err), variant: 'destructive' }); } finally { setOpen(false); setQuery(''); } return; } onSelect(r); }; return ( Search systems
setQuery(e.target.value)} className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-400" />
{results.length === 0 && query && (
No results
)} {results.map((r, idx) => ( ))}
); };