Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 67 additions & 18 deletions src/components/TorrentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function TorrentList() {
const [trackerFilter, setTrackerFilter] = useState<string | null>(null)
const [search, setSearch] = useState('')
const [selected, setSelected] = useState<Set<string>>(new Set())
const [lastSelected, setLastSelected] = useState<string | null>(null)
const [sortKey, setSortKey] = useState<SortKey>(() => {
const stored = localStorage.getItem('sortKey')
if (stored && COLUMNS.some((c) => c.sortKey === stored || stored === 'name')) return stored as SortKey
Expand Down Expand Up @@ -380,17 +381,41 @@ export function TorrentList() {
return filtered.slice(start, start + perPage)
}, [filtered, page, perPage])

function handleSelect(hash: string, multi: boolean) {
setSelected((prev) => {
if (multi) {
const next = new Set(prev)
if (next.has(hash)) next.delete(hash)
else next.add(hash)
return next
function handleSelect(hash: string, multi: boolean, range: boolean) {
if (range && lastSelected && filtered.some((t) => t.hash === lastSelected)) {
const idx1 = filtered.findIndex((t) => t.hash === lastSelected)
const idx2 = filtered.findIndex((t) => t.hash === hash)
if (idx1 !== -1 && idx2 !== -1) {
const start = Math.min(idx1, idx2)
const end = Math.max(idx1, idx2)
const rangeHashes = filtered.slice(start, end + 1).map((t) => t.hash)
setSelected((prev) => {
const next = new Set(prev)
rangeHashes.forEach((h) => next.add(h))
return next
})
}
if (prev.has(hash) && prev.size === 1) return new Set()
return new Set([hash])
})
} else {
setSelected((prev) => {
if (multi) {
const next = new Set(prev)
if (next.has(hash)) next.delete(hash)
else next.add(hash)
return next
}
if (prev.has(hash) && prev.size === 1) return new Set()
return new Set([hash])
})
setLastSelected(hash)
}
}

function handleSelectAll() {
if (selected.size === filtered.length && filtered.length > 0) {
setSelected(new Set())
} else {
setSelected(new Set(filtered.map((t) => t.hash)))
}
}

useEffect(() => {
Expand Down Expand Up @@ -584,14 +609,38 @@ export function TorrentList() {
className="px-4 py-1.5 text-left relative"
style={columnWidths.name ? { width: columnWidths.name } : undefined}
>
<button
onClick={() => handleSort('name')}
className="flex items-center gap-2 text-[9px] font-medium uppercase tracking-widest transition-colors"
style={{ color: 'var(--text-muted)' }}
>
Name
<SortIcon active={sortKey === 'name'} asc={sortAsc} />
</button>
<div className="flex items-center gap-3">
<button
onClick={handleSelectAll}
className="flex items-center justify-center w-4 h-4 rounded border transition-colors"
style={{
borderColor: 'var(--border)',
backgroundColor:
selected.size > 0 ? 'color-mix(in srgb, var(--accent) 10%, transparent)' : 'transparent',
}}
>
{selected.size === filtered.length && filtered.length > 0 && (
<Square className="w-2.5 h-2.5 fill-current" style={{ color: 'var(--accent)' }} />
)}
{selected.size > 0 &&
selected.size < filtered.length &&
// Indeterminate state icon (minus/dash)
(
<div
className="w-2 h-0.5 rounded-full"
style={{ backgroundColor: 'var(--accent)' }}
/>
)}
</button>
<button
onClick={() => handleSort('name')}
className="flex items-center gap-2 text-[9px] font-medium uppercase tracking-widest transition-colors"
style={{ color: 'var(--text-muted)' }}
>
Name
<SortIcon active={sortKey === 'name'} asc={sortAsc} />
</button>
</div>
<div className="resize-handle" onMouseDown={(e) => handleResizeStart(e, 'name')} />
</th>
{orderedColumns
Expand Down
5 changes: 3 additions & 2 deletions src/components/TorrentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ function renderCell(columnId: string, torrent: Torrent, ctx: CellContext): React
interface Props {
torrent: Torrent
selected: boolean
onSelect: (hash: string, multi: boolean) => void
onSelect: (hash: string, multi: boolean, range: boolean) => void
onContextMenu: (e: React.MouseEvent) => void
ratioThreshold: number
hideAddedTime: boolean
Expand Down Expand Up @@ -307,7 +307,8 @@ export function TorrentRow({

return (
<tr
onClick={(e) => onSelect(torrent.hash, e.ctrlKey || e.metaKey)}
onMouseDown={(e) => { if (e.shiftKey) e.preventDefault() }}
onClick={(e) => onSelect(torrent.hash, e.ctrlKey || e.metaKey, e.shiftKey)}
onContextMenu={onContextMenu}
className={`group cursor-pointer transition-colors duration-150 ${isDownloading ? 'downloading' : ''}`}
style={{
Expand Down
Loading