From ef541bbc1b48508775ac4bfd03c6c6af8e47fa63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 29 Jul 2026 23:36:42 +0000 Subject: [PATCH] soulseek: always-visible search delete, clear all, brighter header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-search remove button was hover-gated with opacity-0, so it read as absent until you happened to hover the row. Make it always visible and red on hover, and add a Clear all next to refresh. slskd has no bulk delete (only DELETE /searches/{id}), so clearing fans out one request per search, drops each cached result set, and reconciles with a reload — partial failures surface in a toast. The destructive click arms once and disarms itself after a few seconds instead of opening a modal, matching the other panels, which confirm nothing. That header row sits outside any card, where the muted zinc greys wash out against dark mode's green background — switch the whole level to white. Co-Authored-By: Claude Opus 4.8 --- .../src/apps/Soulseek/SearchView.tsx | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) 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" >