From 36a1159b98de1136a3a827cc79cb15faef267f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Wed, 3 Jun 2026 10:38:16 +0200 Subject: [PATCH] status icon on list --- pytorrent/static/js/torrentRowRenderer.js | 2 +- pytorrent/static/js/torrentTableRenderer.js | 2 +- pytorrent/static/styles.css | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pytorrent/static/js/torrentRowRenderer.js b/pytorrent/static/js/torrentRowRenderer.js index 6eba085..c4cf432 100644 --- a/pytorrent/static/js/torrentRowRenderer.js +++ b/pytorrent/static/js/torrentRowRenderer.js @@ -1 +1 @@ -export const torrentRowRendererSource = " function statusMeta(t){\n const op=activeOperationFor(t);\n if(op) return {cls:'text-bg-info operation-status-badge', icon:actionIcon(op.action), color:'text-info', label:op.label};\n const status=String(t.status||'').toLowerCase();\n if(t.paused || status==='paused') return {cls:'text-bg-warning', icon:'fa-pause', color:'text-warning'};\n if(status==='checking' || Number(t.hashing||0)>0) return {cls:'text-bg-info', icon:'fa-rotate', color:'text-info'};\n if(status==='post-check' || t.post_check) return {cls:'text-bg-dark', icon:'fa-clipboard-check', color:'text-secondary', label:'Post-check'};\n if(status==='seeding') return {cls:'text-bg-success', icon:'fa-seedling', color:'text-success'};\n if(status==='downloading') return {cls:'text-bg-primary', icon:'fa-download', color:'text-primary'};\n if(status==='stopped') return {cls:'text-bg-secondary', icon:'fa-stop', color:'text-secondary'};\n return t.state ? {cls:'text-bg-success', icon:'fa-play', color:'text-success'} : {cls:'text-bg-secondary', icon:'fa-circle', color:'text-secondary'};\n }\n function statusBadge(t){ const m=statusMeta(t); return `${esc(m.label || t.status)}`; }\n function torrentWarning(t){ const msg=String(t.message||'').trim(); if(!msg) return null; const l=msg.toLowerCase(); const patterns=['error','failed','failure','timeout','timed out','tracker','could not','cannot','refused','unreachable','denied']; return patterns.some(p=>l.includes(p)) ? msg : null; }\n function torrentNameIcon(t){ const m=statusMeta(t); return ``; }\n function boolCell(value){ return Number(value||0) ? 'yes' : 'no'; }\n function renderRow(t){\n const labels=labelNames(t.label).map(l=>` ${esc(l)}`).join(' ');\n const warn=torrentWarning(t);\n const op=activeOperationFor(t);\n const classes=[selected.has(t.hash)?'selected':'', t.paused?'torrent-paused':'', op?'torrent-operating':'', warn?'torrent-warning':''].filter(Boolean).join(' ');\n const title=[t.name,warn,op?op.label:''].filter(Boolean).join('\\n');\n return ``+\n ``+\n `${warn?' ':''}${torrentNameIcon(t)} ${esc(t.name)}`+\n `${statusBadge(t)}`+\n `${esc(t.size_h)}`+\n `${progress(t)}`+\n `${esc(t.down_rate_h)}`+\n `${esc(t.up_rate_h)}`+\n `${esc(t.eta_h||\"-\")}`+\n `${esc(t.seeds)}`+\n `${esc(t.peers)}`+\n `${esc(t.ratio)}`+\n `${esc(t.path)}`+\n `${labels||'-'}`+\n `${esc(t.ratio_group||'')}`+\n `${esc(t.down_total_h||'-')}`+\n `${esc(t.to_download_h||'-')}`+\n `${esc(t.up_total_h||'-')}`+\n `${esc(formatDateTime(t.created))}`+\n `${esc(formatDateTime(t.last_activity))}`+\n `${esc(t.priority ?? '-')}`+\n `${boolCell(t.state)}`+\n `${boolCell(t.active)}`+\n `${boolCell(t.complete)}`+\n `${esc(t.hashing ?? 0)}`+\n `${compactCell(t.message||'', 80)}`+\n `${esc(t.hash||'')}`+\n ``;\n }\n\n\n\n\n"; +export const torrentRowRendererSource = " function statusMeta(t){\n const op=activeOperationFor(t);\n if(op) return {cls:'text-bg-info operation-status-badge', icon:actionIcon(op.action), color:'text-info', label:op.label};\n const status=String(t.status||'').toLowerCase();\n if(t.paused || status==='paused') return {cls:'text-bg-warning', icon:'fa-pause', color:'text-warning'};\n if(status==='checking' || Number(t.hashing||0)>0) return {cls:'text-bg-info', icon:'fa-rotate', color:'text-info'};\n if(status==='post-check' || t.post_check) return {cls:'text-bg-dark', icon:'fa-clipboard-check', color:'text-secondary', label:'Post-check'};\n if(status==='seeding') return {cls:'text-bg-success', icon:'fa-seedling', color:'text-success'};\n if(status==='downloading') return {cls:'text-bg-primary', icon:'fa-download', color:'text-primary'};\n if(status==='stopped') return {cls:'text-bg-secondary', icon:'fa-stop', color:'text-secondary'};\n return t.state ? {cls:'text-bg-success', icon:'fa-play', color:'text-success'} : {cls:'text-bg-secondary', icon:'fa-circle', color:'text-secondary'};\n }\n function statusBadge(t){ const m=statusMeta(t); return `${esc(m.label || t.status)}`; }\n function torrentErrorLog(t){\n // Note: The name-column status icon is useful only when the torrent has an error log to show in the tooltip.\n const status=String(t.status||'').trim().toLowerCase();\n const msg=String(t.message||'').trim();\n if(status==='error') return msg || 'Torrent reported an error.';\n if(!msg) return null;\n const patterns=['error','failed','failure','timeout','timed out','tracker','could not','cannot','refused','unreachable','denied'];\n return patterns.some(p=>msg.toLowerCase().includes(p)) ? msg : null;\n }\n function torrentNameStatusIcon(t){\n // Note: Non-error torrents keep the name cell clean; the Status column still shows their normal state.\n const errorLog=torrentErrorLog(t);\n return errorLog ? ` ` : '';\n }\n function boolCell(value){ return Number(value||0) ? 'yes' : 'no'; }\n function renderRow(t){\n const labels=labelNames(t.label).map(l=>` ${esc(l)}`).join(' ');\n const errorLog=torrentErrorLog(t);\n const op=activeOperationFor(t);\n const classes=[selected.has(t.hash)?'selected':'', t.paused?'torrent-paused':'', op?'torrent-operating':'', errorLog?'torrent-warning':''].filter(Boolean).join(' ');\n const title=[t.name,errorLog,op?op.label:''].filter(Boolean).join('\\n');\n return ``+\n ``+\n `${torrentNameStatusIcon(t)}${esc(t.name)}`+\n `${statusBadge(t)}`+\n `${esc(t.size_h)}`+\n `${progress(t)}`+\n `${esc(t.down_rate_h)}`+\n `${esc(t.up_rate_h)}`+\n `${esc(t.eta_h||\"-\")}`+\n `${esc(t.seeds)}`+\n `${esc(t.peers)}`+\n `${esc(t.ratio)}`+\n `${esc(t.path)}`+\n `${labels||'-'}`+\n `${esc(t.ratio_group||'')}`+\n `${esc(t.down_total_h||'-')}`+\n `${esc(t.to_download_h||'-')}`+\n `${esc(t.up_total_h||'-')}`+\n `${esc(formatDateTime(t.created))}`+\n `${esc(formatDateTime(t.last_activity))}`+\n `${esc(t.priority ?? '-')}`+\n `${boolCell(t.state)}`+\n `${boolCell(t.active)}`+\n `${boolCell(t.complete)}`+\n `${esc(t.hashing ?? 0)}`+\n `${compactCell(t.message||'', 80)}`+\n `${esc(t.hash||'')}`+\n ``;\n }\n\n\n\n\n"; diff --git a/pytorrent/static/js/torrentTableRenderer.js b/pytorrent/static/js/torrentTableRenderer.js index a98712a..6516730 100644 --- a/pytorrent/static/js/torrentTableRenderer.js +++ b/pytorrent/static/js/torrentTableRenderer.js @@ -1 +1 @@ -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 warn=torrentWarning(t);\n const op=activeOperationFor(t);\n const classes=[selected.has(t.hash)?'selected':'', op?'torrent-operating':'', warn?'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 `
${warn?' ':''}${torrentNameIcon(t)} ${esc(t.name)}
${lines.primary?`
${lines.primary}
`:''}${lines.secondary?`
${lines.secondary}
`:''}${mobileColumns.path?`
${esc(t.path)}
`:''}
${mobileColumns.progress?`
${progress(t)}
`:''}
`;\n }).join('') || (hasTorrentSnapshot ? `
No torrents.
` : loadingMarkup('Loading torrents...'));\n }\n function renderTable(){ updateBulkBar(); syncActiveFilterSelection(); renderCounts(); renderLabelFilters(); if(typeof renderHealthDashboard==='function') renderHealthDashboard(); if(typeof renderSmartViewsManager==='function') renderSmartViewsManager(); 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 rowHeight=torrentRowHeight(); const start=Math.max(0,Math.floor((wrap?.scrollTop||0)/rowHeight)-OVERSCAN); const count=Math.ceil((wrap?.clientHeight||500)/rowHeight)+OVERSCAN*2; const end=Math.min(visibleRows.length,start+count); const sig=`${renderVersion}:${start}:${end}:${visibleRows.length}:${sortState.key}:${sortState.dir}:${selected.size}:${activeFilter}:${activeTrackerFilter}:${compactTorrentListEnabled?1:0}:${$('searchBox')?.value||''}:${[...selected].slice(0,30).join(',')}`; if(sig===lastRenderSignature) return; lastRenderSignature=sig; const top=start*rowHeight,bottom=Math.max(0,(visibleRows.length-end)*rowHeight); body.innerHTML=(top?``:'')+visibleRows.slice(start,end).map(renderRow).join('')+(bottom?``:''); applyColumnVisibility(); }\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"; +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 `
${torrentNameStatusIcon(t)}${esc(t.name)}
${lines.primary?`
${lines.primary}
`:''}${lines.secondary?`
${lines.secondary}
`:''}${mobileColumns.path?`
${esc(t.path)}
`:''}
${mobileColumns.progress?`
${progress(t)}
`:''}
`;\n }).join('') || (hasTorrentSnapshot ? `
No torrents.
` : loadingMarkup('Loading torrents...'));\n }\n function renderTable(){ updateBulkBar(); syncActiveFilterSelection(); renderCounts(); renderLabelFilters(); if(typeof renderHealthDashboard==='function') renderHealthDashboard(); if(typeof renderSmartViewsManager==='function') renderSmartViewsManager(); 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 rowHeight=torrentRowHeight(); const start=Math.max(0,Math.floor((wrap?.scrollTop||0)/rowHeight)-OVERSCAN); const count=Math.ceil((wrap?.clientHeight||500)/rowHeight)+OVERSCAN*2; const end=Math.min(visibleRows.length,start+count); const sig=`${renderVersion}:${start}:${end}:${visibleRows.length}:${sortState.key}:${sortState.dir}:${selected.size}:${activeFilter}:${activeTrackerFilter}:${compactTorrentListEnabled?1:0}:${$('searchBox')?.value||''}:${[...selected].slice(0,30).join(',')}`; if(sig===lastRenderSignature) return; lastRenderSignature=sig; const top=start*rowHeight,bottom=Math.max(0,(visibleRows.length-end)*rowHeight); body.innerHTML=(top?``:'')+visibleRows.slice(start,end).map(renderRow).join('')+(bottom?``:''); applyColumnVisibility(); }\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"; diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css index c8a6d18..8587950 100644 --- a/pytorrent/static/styles.css +++ b/pytorrent/static/styles.css @@ -1924,6 +1924,7 @@ body.mobile-mode .mobile-card { } .torrent-warning-icon { color: var(--bs-warning); + cursor: help; margin-right: 0.2rem; } .mobile-filter-bar {