diff --git a/src/workspaces/officerdev/src/apps/Soulseek/SearchView.tsx b/src/workspaces/officerdev/src/apps/Soulseek/SearchView.tsx index 0166c0ea..8197461d 100644 --- a/src/workspaces/officerdev/src/apps/Soulseek/SearchView.tsx +++ b/src/workspaces/officerdev/src/apps/Soulseek/SearchView.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useClient } from 'hooks/useClient'; import { toast } from 'sonner'; -import { Search, Loader2, RefreshCw, X, History, FileAudio, Users, ChevronRight } from 'lucide-react'; +import { Search, Loader2, RefreshCw, X, History, FileAudio, Users, ChevronRight, Trash2 } from 'lucide-react'; import { SearchResults, dropCachedResults } from './SearchResults'; import { formatWhen, type SlskdSearchSummary } from './shared'; @@ -15,6 +15,8 @@ export const SearchView = () => { const [history, setHistory] = useState(null); const [error, setError] = useState(null); const [selected, setSelected] = useState(null); + const [clearing, setClearing] = useState(false); + const [confirmClear, setConfirmClear] = useState(false); const load = useCallback(() => { setError(null); @@ -52,6 +54,30 @@ export const SearchView = () => { client.delete(`/slskd/api/v0/searches/${id}`).catch(() => load()); }; + // slskd has no bulk delete (0.26.0 exposes only DELETE /searches/{id}), so clearing fans out one + // request per search and reconciles with a reload — partial failures come back on their own. + const clearAll = async () => { + const ids = history?.map((s) => s.id) ?? []; + if (!ids.length || clearing) return; + setConfirmClear(false); + setClearing(true); + setHistory([]); + ids.forEach(dropCachedResults); + const settled = await Promise.allSettled(ids.map((id) => client.delete(`/slskd/api/v0/searches/${id}`))); + const failed = settled.filter((r) => r.status === 'rejected').length; + if (failed) toast.error(`${failed} of ${ids.length} searches could not be removed.`); + setClearing(false); + load(); + }; + + // The destructive click is two-step rather than a modal (matching the other Soulseek panels, which + // confirm nothing) — and the armed state disarms itself so it can't sit there waiting to be hit. + useEffect(() => { + if (!confirmClear) return; + const timer = setTimeout(() => setConfirmClear(false), 4000); + return () => clearTimeout(timer); + }, [confirmClear]); + if (selected) return setSelected(null)} />; return ( @@ -82,19 +108,36 @@ export const SearchView = () => { {/* History */}
-
+
Recent searches - {history && · {history.length}} + {history && · {history.length}} +
+
+ {!!history?.length && ( + + )} +
-
@@ -124,7 +167,7 @@ export const SearchView = () => { setSelected(s); } }} - className="group flex cursor-pointer items-center gap-3 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2.5 shadow-sm transition-colors hover:bg-white/5" + className="flex cursor-pointer items-center gap-3 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2.5 shadow-sm transition-colors hover:bg-white/5" >
{s.isComplete ? : } @@ -149,8 +192,8 @@ export const SearchView = () => { ev.stopPropagation(); remove(s.id); }} - title="Remove" - className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-400 opacity-0 transition hover:bg-white/10 hover:text-zinc-100 group-hover:opacity-100" + title="Remove search" + className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-500 transition hover:bg-red-500/10 hover:text-red-400" >