Files
pyTorrent/pytorrent/static/js/torrentTableRenderer.js
T
2026-06-12 23:20:42 +02:00

2 lines
7.1 KiB
JavaScript

export const torrentTableRendererSource = " function renderMobile(){\n const list=$('mobileList');\n if(!list) return;\n const src=mobileVisibleRows();\n const rows=src.slice(0,250);\n renderMobileFilters(src);\n list.innerHTML=rows.map(t=>{\n const errorLog=torrentErrorLog(t);\n const op=activeOperationFor(t);\n const classes=[selected.has(t.hash)?'selected':'', op?'torrent-operating':'', errorLog?'torrent-warning':''].filter(Boolean).join(' ');\n const lines=mobileInfoLines(t);\n // Note: Mobile details use a separate corner button so user-configurable action buttons keep their current order.\n return `<div class=\"mobile-card ${classes}\" data-hash=\"${esc(t.hash)}\" title=\"${esc(errorLog||op?.label||'')}\"><div class=\"mobile-card-header\"><div class=\"name\">${torrentNameStatusIcon(t)}${esc(t.name)}</div><button class=\"btn btn-xs btn-outline-info mobile-details-btn\" type=\"button\" title=\"Torrent details\" aria-label=\"Open torrent details\"><i class=\"fa-solid fa-circle-info\"></i></button></div>${lines.primary?`<div class=\"small text-muted\">${lines.primary}</div>`:''}${lines.secondary?`<div class=\"small\">${lines.secondary}</div>`:''}${mobileColumns.path?`<div class=\"small text-truncate\">${esc(t.path)}</div>`:''}<div class=\"mobile-actions\"><button class=\"btn btn-xs btn-outline-success\" data-action=\"start\" title=\"Start\"><i class=\"fa-solid fa-play\"></i></button><button class=\"btn btn-xs btn-outline-warning\" data-action=\"pause\" title=\"Pause\"><i class=\"fa-solid fa-pause\"></i></button><button class=\"btn btn-xs btn-outline-secondary\" data-action=\"stop\" title=\"Stop\"><i class=\"fa-solid fa-stop\"></i></button><button class=\"btn btn-xs btn-outline-primary\" data-action=\"move\" title=\"Move\"><i class=\"fa-solid fa-folder-open\"></i></button><button class=\"btn btn-xs btn-outline-primary\" data-mobile-modal=\"label\" title=\"Set label\"><i class=\"fa-solid fa-tag\"></i></button><button class=\"btn btn-xs btn-outline-info\" data-action=\"recheck\" title=\"Force recheck\"><i class=\"fa-solid fa-rotate\"></i></button><button class=\"btn btn-xs btn-outline-primary\" data-action=\"reannounce\" title=\"Reannounce\"><i class=\"fa-solid fa-bullhorn\"></i></button><button class=\"btn btn-xs btn-outline-danger\" data-action=\"remove\" title=\"Remove\"><i class=\"fa-solid fa-trash-can\"></i></button></div>${mobileColumns.progress?`<div class=\"mobile-progress\">${progress(t)}</div>`:''}</div>`;\n }).join('') || (hasTorrentSnapshot ? `<div class=\"empty\">No torrents.</div>` : loadingMarkup('Loading torrents...'));\n }\n\n\n function torrentHeaderHeight(){\n const header=document.querySelector('.torrent-table thead');\n return Math.ceil(header?.getBoundingClientRect?.().height || 0);\n }\n\n function clearVirtualBody(body){\n body.classList.remove('torrent-virtual-body');\n body.style.height='';\n }\n\n function renderTable(){\n updateBulkBar();\n syncActiveFilterSelection();\n renderCounts();\n renderLabelFilters();\n if(typeof renderHealthDashboard==='function') renderHealthDashboard();\n if(typeof renderSmartViewsManager==='function') renderSmartViewsManager();\n updateSortHeaders();\n buildVisibleRows();\n renderMobile();\n\n const body=$('torrentBody');\n if(!body) return;\n\n if(!visibleRows.length){\n clearVirtualBody(body);\n body.innerHTML=hasTorrentSnapshot?`<tr><td colspan=\"${torrentColumnSpan()}\" class=\"empty\">No torrents for this filter.</td></tr>`:loadingTableRow('Loading torrents...');\n applyColumnVisibility();\n return;\n }\n\n const wrap=$('tableWrap');\n const rowHeight=torrentRowHeight();\n const headerHeight=torrentHeaderHeight();\n const scrollTop=Math.max(0, (wrap?.scrollTop || 0) - headerHeight);\n const viewportHeight=Math.max(rowHeight, (wrap?.clientHeight || 500) - headerHeight);\n const maxStart=Math.max(0, visibleRows.length - 1);\n const start=Math.min(maxStart, Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN));\n const visibleCount=Math.ceil(viewportHeight / rowHeight) + OVERSCAN * 2 + 1;\n const end=Math.min(visibleRows.length, start + visibleCount);\n const totalHeight=visibleRows.length * rowHeight;\n\n const sig=`${renderVersion}:${start}:${end}:${totalHeight}:${rowHeight}:${sortState.key}:${sortState.dir}:${selected.size}:${activeFilter}:${activeTrackerFilter}:${compactTorrentListEnabled?1:0}:${$('searchBox')?.value||''}:${[...selected].slice(0,30).join(',')}`;\n if(sig===lastRenderSignature) return;\n\n lastRenderSignature=sig;\n body.classList.add('torrent-virtual-body');\n body.style.height=`${totalHeight}px`;\n // Note: Rows are absolutely positioned inside one fixed-height body, so the browser keeps a stable native scroll range even at 50k torrents.\n body.innerHTML=visibleRows.slice(start,end).map((torrent,index)=>renderRow(torrent,{className:'torrent-virtual-row',style:`transform:translateY(${(start+index)*rowHeight}px)`})).join('');\n applyColumnVisibility();\n }\n\n function scheduleRender(force=false){ if(force){lastRenderSignature='';renderVersion++;} if(renderPending)return; renderPending=true; requestAnimationFrame(()=>{renderPending=false;renderTable();}); }\n function patchRows(msg){ if(msg.summary) torrentSummary=msg.summary; (msg.removed||[]).forEach(h=>{torrents.delete(h);selected.delete(h);activeOperations.delete(h);if(selectedHash===h)selectedHash=null;}); (msg.added||[]).forEach(t=>torrents.set(t.hash,t)); (msg.updated||[]).forEach(p=>torrents.set(p.hash,{...(torrents.get(p.hash)||{}),...p})); if(msg.speed_status) applyLiveSpeedStats(msg.speed_status); else updateBrowserSpeedTitle(); scheduleRender(true); if(selectedHash&&torrents.has(selectedHash)&&activeTab()==='general') renderGeneral(); }\n function applyLiveTorrentStats(msg){ (msg.updated||[]).forEach(p=>{ if(torrents.has(p.hash)) torrents.set(p.hash,{...(torrents.get(p.hash)||{}),...p}); }); if(msg.speed_status) applyLiveSpeedStats(msg.speed_status); else updateBrowserSpeedTitle(); scheduleRender(true); if(selectedHash&&torrents.has(selectedHash)&&activeTab()==='general') renderGeneral(); }\n function selectedHashes(){ return [...selected]; }\n function updateBulkBar(){\n const bar=$(\"bulkBar\");\n if(!bar) return;\n // Note: The desktop bulk toolbar is hidden in mobile mode; mobile has its own compact actions in the filter bar.\n const isMobileMode = document.body.classList.contains('mobile-mode');\n const show = selected.size > 1 && !isMobileMode;\n bar.classList.toggle(\"d-none\", !show);\n bar.setAttribute('aria-hidden', show ? 'false' : 'true');\n const c=$(\"bulkSelectedCount\");\n if(c) c.textContent=selected.size;\n }\n function setSelectionRange(hash, keepExisting=false){ const current=visibleRows.findIndex(t=>t.hash===hash); const last=visibleRows.findIndex(t=>t.hash===lastSelectedHash); if(current<0 || last<0){ selected.add(hash); lastSelectedHash=hash; return; } if(!keepExisting) selected.clear(); const a=Math.min(current,last), b=Math.max(current,last); visibleRows.slice(a,b+1).forEach(t=>selected.add(t.hash)); selectedHash=hash; }\n";