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 { useState, useEffect, useCallback } from 'react';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { toast } from 'sonner';
|
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 { SearchResults, dropCachedResults } from './SearchResults';
|
||||||
import { formatWhen, type SlskdSearchSummary } from './shared';
|
import { formatWhen, type SlskdSearchSummary } from './shared';
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ export const SearchView = () => {
|
|||||||
const [history, setHistory] = useState<SlskdSearchSummary[] | null>(null);
|
const [history, setHistory] = useState<SlskdSearchSummary[] | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selected, setSelected] = useState<SlskdSearchSummary | null>(null);
|
const [selected, setSelected] = useState<SlskdSearchSummary | null>(null);
|
||||||
|
const [clearing, setClearing] = useState(false);
|
||||||
|
const [confirmClear, setConfirmClear] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -52,6 +54,30 @@ export const SearchView = () => {
|
|||||||
client.delete(`/slskd/api/v0/searches/${id}`).catch(() => load());
|
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)} />;
|
if (selected) return <SearchResults search={selected} onBack={() => setSelected(null)} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,20 +108,37 @@ export const SearchView = () => {
|
|||||||
|
|
||||||
{/* History */}
|
{/* History */}
|
||||||
<div className="flex shrink-0 items-center justify-between px-6 pb-2 pt-4">
|
<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" />
|
<History className="h-3.5 w-3.5" />
|
||||||
Recent searches
|
Recent searches
|
||||||
{history && <span className="text-zinc-600">· {history.length}</span>}
|
{history && <span>· {history.length}</span>}
|
||||||
</div>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={load}
|
onClick={load}
|
||||||
title="Refresh"
|
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"
|
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" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">
|
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">
|
||||||
{error && <p className="px-2 py-3 text-sm text-red-500">Could not load history: {error}</p>}
|
{error && <p className="px-2 py-3 text-sm text-red-500">Could not load history: {error}</p>}
|
||||||
@@ -124,7 +167,7 @@ export const SearchView = () => {
|
|||||||
setSelected(s);
|
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">
|
<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" />}
|
{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();
|
ev.stopPropagation();
|
||||||
remove(s.id);
|
remove(s.id);
|
||||||
}}
|
}}
|
||||||
title="Remove"
|
title="Remove search"
|
||||||
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"
|
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" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user