import { useState, useEffect, useMemo } from 'react'; import { Link } from 'react-router'; import { useClient } from 'hooks/useClient'; import { ArrowDownToLine, ArrowUpFromLine, Search, RefreshCw, Radio } from 'lucide-react'; import { SEARCH_PARAM, formatWhen, soulseekSectionPath, transferPhase, toneSolid, PHASE_TONE, type SlskdApplication, type SlskdSearchSummary, type SlskdTransferUser, type TransferPhase, } from './shared'; // Dashboard — the at-a-glance overview for the /soulseek workspace: connection health, live // download/upload tallies, and the most recent searches. Read-only aggregation over the same slskd // endpoints the other panels use; every tile and row is a real link into the section it summarises, and // a recent search links to that search rather than to the search screen's front page. const POLL_MS = 2000; type Tally = Record; const emptyTally = (): Tally => ({ downloading: 0, queued: 0, done: 0, failed: 0 }); const tally = (users: SlskdTransferUser[]): Tally => { const t = emptyTally(); for (const u of users) for (const d of u.directories ?? []) for (const f of d.files ?? []) t[transferPhase(f.state)]++; return t; }; export const SoulseekDashboard = () => { const client = useClient(); const [app, setApp] = useState(null); const [downloads, setDownloads] = useState(emptyTally); const [uploads, setUploads] = useState(emptyTally); const [searches, setSearches] = useState([]); useEffect(() => { let cancelled = false; const tick = async () => { const [a, d, u, s] = await Promise.allSettled([ client.get('/soulseek/api/v0/application'), client.get('/soulseek/api/v0/transfers/downloads'), client.get('/soulseek/api/v0/transfers/uploads'), client.get('/soulseek/api/v0/searches'), ]); if (cancelled) return; if (a.status === 'fulfilled') setApp(a.value); if (d.status === 'fulfilled') setDownloads(tally(d.value)); if (u.status === 'fulfilled') setUploads(tally(u.value)); if (s.status === 'fulfilled') setSearches([...s.value].sort((x, y) => new Date(y.startedAt).getTime() - new Date(x.startedAt).getTime())); }; tick(); const timer = setInterval(tick, POLL_MS); return () => { cancelled = true; clearInterval(timer); }; // useClient() is a fresh object each render — poll on a stable interval only. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const server = app?.server; const connected = server?.isConnected ?? false; const recent = useMemo(() => searches.slice(0, 6), [searches]); return (
{/* Connection */}
{server?.state ?? 'Unknown'}
{server?.address &&
{server.address}
}
{app?.version?.current && ( v{app.version.current} {app.version.isUpdateAvailable ? ' · update available' : ''} )}
{/* Downloads */}
} to={soulseekSectionPath('downloads')}>
{/* Uploads */}
} to={soulseekSectionPath('uploads')}>
{/* Recent searches */}
Recent searches View all
{recent.length === 0 ? (

No searches yet.

) : (
{recent.map((s) => ( {s.searchText} {s.responseCount} responses {formatWhen(s.startedAt)} ))}
)}
); }; type SectionProps = { title: string; icon: React.ReactNode; to: string; children: React.ReactNode }; const Section = ({ title, icon, to, children }: SectionProps) => (
{icon} {title}
{children}
); type TileProps = { label: string; value: number; dot: string }; const Tile = ({ label, value, dot }: TileProps) => (
{label}
{value}
);