import type { LucideIcon } from 'lucide-react'; import type { FacetEntry } from './useTorrentFilters'; import { useState } from 'react'; import { NavLink } from 'react-router'; import { ArrowDownToLine, ArrowUpFromLine, ChevronRight, CircleAlert, Folder, Gauge, ListChecks, Plug, Radio, Settings, Tag, Waves, } from 'lucide-react'; import { TRANSMISSION_SECTIONS, transmissionSectionPath, type TransmissionSectionId } from './shared'; import { formatSpeed } from './format'; import { useSessionStats, useTorrents } from './useTransmissionData'; import { buildFacets, countByStatus, STATUS_FILTERS, useTorrentFilters } from './useTorrentFilters'; // Left panel of /transmission: live throughput on top, the three sections, then the facet filters. // // Sections are real links (cmd-click, back button, reload) with active state from react-router's NavLink. // Filters are buttons because they write to the query string of the page you are already on — they are // still addressable, just not a different route. // // The facet lists are derived from the torrents already in the cache, not fetched separately: Transmission // has no "list my labels" RPC, and asking for one would mean a second poll returning the same data. const ICONS: Record = { torrents: ListChecks, stats: Gauge, settings: Settings, connection: Plug, }; const ROW = 'group relative flex items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors'; export const TransmissionNav = () => { const { torrents } = useTorrents(); const { stats } = useSessionStats(); const { filters, setStatus, toggle, clearFilters } = useTorrentFilters(); const facets = buildFacets(torrents); const counts = countByStatus(torrents); return (
Transmission
{formatSpeed(stats?.downloadSpeed) || '0'} {formatSpeed(stats?.uploadSpeed) || '0'}
Filter {filters.isFiltered && ( )}
{STATUS_FILTERS.map(({ id, label }) => ( setStatus(id)} /> ))} {facets.errors > 0 && ( toggle('error', '1', filters.errorOnly ? '1' : null)} /> )}
toggle('label', v, filters.label)} /> toggle('tracker', v, filters.tracker)} /> toggle('dir', v, filters.downloadDir)} // A path is only distinguishable by its tail, and the panel is 22% of the window wide. renderLabel={(v) => v.split('/').filter(Boolean).slice(-1)[0] || v} />
{stats ? `${stats.activeTorrentCount} active · ${stats.pausedTorrentCount} paused` : 'connecting…'}
); }; type FilterRowProps = { label: string; count: number; active: boolean; onClick: () => void; icon?: LucideIcon; tone?: string; title?: string; }; const FilterRow = ({ label, count, active, onClick, icon: Icon, tone, title }: FilterRowProps) => ( ); type FacetGroupProps = { title: string; icon: LucideIcon; entries: FacetEntry[]; selected: string | null; onPick: (value: string) => void; renderLabel?: (value: string) => string; }; /** * Collapsible because these lists are open-ended — one tracker per site, one label per whim. Collapsed by * default unless something in the group is selected, so arriving on a shared filtered URL shows you why. */ const FacetGroup = ({ title, icon: Icon, entries, selected, onPick, renderLabel }: FacetGroupProps) => { const [open, setOpen] = useState(false); const expanded = open || selected != null; if (!entries.length) return null; return (
{expanded && (
{entries.map(({ value, count }) => ( onPick(value)} /> ))}
)}
); };