From dd09b428fa57ab1097ddbddb1633872c5ccc74b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 8 May 2026 23:02:19 +0200 Subject: [PATCH] fix render --- pytorrent/static/app.js | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js index 893d85c..717fd6e 100644 --- a/pytorrent/static/app.js +++ b/pytorrent/static/app.js @@ -24,6 +24,9 @@ let trackerSummaryStatus = 'idle'; let trackerSummarySignature = ""; let trackerSummaryTimer = null; + let lastLabelFiltersSignature = ""; + let lastTrackerFiltersSignature = ""; + let lastMobileFiltersSignature = ""; const BASE_TITLE = document.title || "pyTorrent"; const lastBrowserSpeed = {down: "0 B/s", up: "0 B/s"}; const FOOTER_ITEM_DEFS = [ @@ -226,21 +229,24 @@ scheduleRender(true); })); } - function renderLabelFilters(){ + function renderLabelFilters(force=false){ const box=$('labelFilters'); if(!box) return; const counts=new Map(); [...torrents.values()].forEach(t=>labelNames(t.label).forEach(l=>counts.set(l,(counts.get(l)||0)+1))); const labels=[...counts.keys()].filter(l=>counts.get(l)>0).sort((a,b)=>a.localeCompare(b)); if(activeFilter.startsWith('label:') && !counts.has(activeFilter.slice(6))) activeFilter='all'; + const sig=labels.map(l=>`${l}:${counts.get(l)}`).join('|'); + if(!force && sig===lastLabelFiltersSignature){ syncFilterButtons(); return; } + lastLabelFiltersSignature=sig; box.innerHTML=labels.length?`
Labels
${labels.map(l=>``).join('')}`:''; bindSidebarFilterClicks(box); } function trackerFavicon(tracker){ const domain=typeof tracker==='string'?tracker:(tracker?.domain||''); if(!trackerFaviconsEnabled || !domain) return ''; - // Note: Cached favicons are served from the static/tracker_favicons symlink; the API path is only a one-time cache warmer fallback. - const fallback=`/api/trackers/favicon/${encodeURIComponent(domain)}?refresh=1`; + // Note: Normal rendering must use cached/static URLs only. Avoid refresh=1 here, otherwise scroll-triggered paints can re-warm icons repeatedly. + const fallback=`/api/trackers/favicon/${encodeURIComponent(domain)}`; const src=(typeof tracker==='object' && tracker?.favicon_url) ? tracker.favicon_url : fallback; return ``; } @@ -251,11 +257,21 @@ if(hasTorrentSnapshot && torrents.size) return '
No trackers found
'; return '
Waiting for torrents...
'; } - function renderTrackerFilters(){ + function renderTrackerFilters(force=false){ const box=$('trackerFilters'); if(!box) return; const trackers=trackerSummary.trackers || []; if(activeFilter.startsWith('tracker:') && !trackers.some(t=>t.domain===activeFilter.slice(8))) activeFilter='all'; + const sig=[ + trackerSummaryStatus, + trackerFaviconsEnabled ? 1 : 0, + trackerSummary.pending || 0, + trackerSummary.cached || 0, + trackerSummary.scanned || 0, + trackers.map(t=>`${t.domain}:${t.count||0}:${t.favicon_url||''}`).join('|') + ].join('::'); + if(!force && sig===lastTrackerFiltersSignature){ syncFilterButtons(); return; } + lastTrackerFiltersSignature=sig; // Note: Tracker filter section is always visible, so an empty or failed tracker scan does not look like a missing feature. const rows=trackers.length ? trackers.map(t=>``).join('') @@ -336,13 +352,17 @@ if(!bar) return; const allVisible=visibleRows.length>0 && visibleRows.every(t=>selected.has(t.hash)); const someVisible=visibleRows.some(t=>selected.has(t.hash)); - const opts=mobileFilterDefs().map(([key,label,count,type])=>``).join(''); + const defs=mobileFilterDefs(); + const sig=[activeFilter, selected.size, allVisible ? 1 : 0, someVisible ? 1 : 0, defs.map(d=>`${d[0]}:${d[2]}`).join('|')].join('::'); + if(sig===lastMobileFiltersSignature) return; + lastMobileFiltersSignature=sig; + const opts=defs.map(([key,label,count,type])=>``).join(''); const bulk=selected.size?``:''; // Note: Mobile bulk actions reuse the existing label modal and move picker, so desktop behavior stays unchanged. bar.innerHTML=`
${bulk}${selected.size} selected
`; } function renderMobile(){ const list=$('mobileList'); if(!list) return; const src=visibleRows.length?visibleRows:[...torrents.values()].filter(rowVisible).sort(compareRows); const rows=src.slice(0,250); renderMobileFilters(); list.innerHTML=rows.map(t=>{ const warn=torrentWarning(t); const op=activeOperationFor(t); const classes=[selected.has(t.hash)?'selected':'', op?'torrent-operating':'', warn?'torrent-warning':''].filter(Boolean).join(' '); return `
${warn?' ':''}${torrentNameIcon(t)} ${esc(t.name)}
${statusBadge(t)} · ${esc(t.progress)}% · Ratio ${esc(t.ratio)}
DL ${esc(t.down_rate_h)} / UL ${esc(t.up_rate_h)}
${esc(t.path)}
${progress(t)}
`; }).join('') || (hasTorrentSnapshot ? `
No torrents.
` : loadingMarkup('Loading torrents...')); } - function renderTable(){ updateBulkBar(); renderCounts(); renderLabelFilters(); renderTrackerFilters(); updateSortHeaders(); buildVisibleRows(); renderMobile(); const body=$('torrentBody'); if(!visibleRows.length){ body.innerHTML=hasTorrentSnapshot?'No torrents for this filter.':loadingTableRow('Loading torrents...'); return; } const wrap=$('tableWrap'); const start=Math.max(0,Math.floor((wrap?.scrollTop||0)/ROW_HEIGHT)-OVERSCAN); const count=Math.ceil((wrap?.clientHeight||500)/ROW_HEIGHT)+OVERSCAN*2; const end=Math.min(visibleRows.length,start+count); const sig=`${renderVersion}:${start}:${end}:${visibleRows.length}:${sortState.key}:${sortState.dir}:${selected.size}:${activeFilter}:${$('searchBox')?.value||''}:${[...selected].slice(0,30).join(',')}`; if(sig===lastRenderSignature) return; lastRenderSignature=sig; const top=start*ROW_HEIGHT,bottom=Math.max(0,(visibleRows.length-end)*ROW_HEIGHT); body.innerHTML=(top?``:'')+visibleRows.slice(start,end).map(renderRow).join('')+(bottom?``:''); applyColumnVisibility(); } + function renderTable(){ updateBulkBar(); renderCounts(); renderLabelFilters(); updateSortHeaders(); buildVisibleRows(); renderMobile(); const body=$('torrentBody'); if(!visibleRows.length){ body.innerHTML=hasTorrentSnapshot?'No torrents for this filter.':loadingTableRow('Loading torrents...'); return; } const wrap=$('tableWrap'); const start=Math.max(0,Math.floor((wrap?.scrollTop||0)/ROW_HEIGHT)-OVERSCAN); const count=Math.ceil((wrap?.clientHeight||500)/ROW_HEIGHT)+OVERSCAN*2; const end=Math.min(visibleRows.length,start+count); const sig=`${renderVersion}:${start}:${end}:${visibleRows.length}:${sortState.key}:${sortState.dir}:${selected.size}:${activeFilter}:${$('searchBox')?.value||''}:${[...selected].slice(0,30).join(',')}`; if(sig===lastRenderSignature) return; lastRenderSignature=sig; const top=start*ROW_HEIGHT,bottom=Math.max(0,(visibleRows.length-end)*ROW_HEIGHT); body.innerHTML=(top?``:'')+visibleRows.slice(start,end).map(renderRow).join('')+(bottom?``:''); applyColumnVisibility(); } function scheduleRender(force=false){ if(force){lastRenderSignature='';renderVersion++;} if(renderPending)return; renderPending=true; requestAnimationFrame(()=>{renderPending=false;renderTable();}); } 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})); scheduleRender(true); if(selectedHash&&torrents.has(selectedHash)&&activeTab()==='general') renderGeneral(); } function selectedHashes(){ return [...selected]; }