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]; }