soulseek: always-visible search delete, clear all, brighter header
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<SlskdSearchSummary[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<SlskdSearchSummary | null>(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 <SearchResults search={selected} onBack={() => setSelected(null)} />;
|
||||
|
||||
return (
|
||||
@@ -82,19 +108,36 @@ export const SearchView = () => {
|
||||
|
||||
{/* History */}
|
||||
<div className="flex shrink-0 items-center justify-between px-6 pb-2 pt-4">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-white">
|
||||
<History className="h-3.5 w-3.5" />
|
||||
Recent searches
|
||||
{history && <span className="text-zinc-600">· {history.length}</span>}
|
||||
{history && <span>· {history.length}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{!!history?.length && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (confirmClear ? clearAll() : setConfirmClear(true))}
|
||||
onBlur={() => setConfirmClear(false)}
|
||||
disabled={clearing}
|
||||
title={confirmClear ? `Delete all ${history.length} searches` : 'Clear all searches'}
|
||||
className={`flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-white transition disabled:opacity-50 ${
|
||||
confirmClear ? 'bg-red-500/20 hover:bg-red-500/30' : 'hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
{clearing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
|
||||
{clearing ? 'Clearing…' : confirmClear ? `Delete ${history.length}?` : 'Clear all'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={load}
|
||||
title="Refresh"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-white transition hover:bg-white/10"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={load}
|
||||
title="Refresh"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-zinc-500 transition hover:bg-white/5 hover:text-zinc-100"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">
|
||||
@@ -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"
|
||||
>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/5 text-zinc-400">
|
||||
{s.isComplete ? <Search className="h-4 w-4" /> : <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
@@ -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"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user