From ef8585fe666a94b143170a498e11e62a0a39636a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 15 Jun 2026 11:03:25 +0200 Subject: [PATCH] fix in js --- pytorrent/static/js/torrentFilterUi.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pytorrent/static/js/torrentFilterUi.js b/pytorrent/static/js/torrentFilterUi.js index 536e4b4..426f2b1 100644 --- a/pytorrent/static/js/torrentFilterUi.js +++ b/pytorrent/static/js/torrentFilterUi.js @@ -1,2 +1 @@ -export const torrentFilterUiSource = " function movingOperationRows(){\n // Note: The Moving filter is based only on active move operations, not queued jobs.\n return [...torrents.values()].filter(t=>{\n const op=activeOperationFor(t);\n return op?.action==='move' && op?.state==='running';\n });\n }\n function movingFilterCount(){ return movingOperationRows().length; }\n function torrentMatchesFilterType(t, type){\n if(type==='all') return true;\n if(type==='downloading') return !isChecking(t) && !t.complete && t.state && !t.paused && t.status!=='Queued'; - if(type==='queued') return !!t.queued || t.status==='Queued';\n if(type==='seeding') return !isChecking(t) && t.complete && t.state && !t.paused;\n if(type==='paused') return !!t.paused || t.status==='Paused';\n if(type==='checking') return isChecking(t);\n if(type==='error') return torrentHasError(t);\n if(type==='post_check') return t.status==='Post-check' || !!t.post_check;\n if(type==='stopped') return !t.state && !isChecking(t) && t.status!=='Post-check' && !t.post_check;\n if(type==='moving'){\n const op=activeOperationFor(t);\n return op?.action==='move' && op?.state==='running';\n }\n return true;\n }\n function trackerScopedRows(){\n const rows=[...torrents.values()];\n return activeTrackerFilter ? rows.filter(t=>rowHasTracker(t, activeTrackerFilter)) : rows;\n }\n function summarizeFilterRows(rows, type){\n const matched=rows.filter(t=>torrentMatchesFilterType(t, type));\n const bucket={count:matched.length,size:0,disk_bytes:0,completed_bytes:0,remaining_bytes:0};\n matched.forEach(t=>{\n const size=Number(t.size||0);\n const progress=Number(t.progress||0);\n const completed=Number(t.completed_bytes ?? t.completed ?? t.down_total ?? (size && Number.isFinite(progress) ? size * Math.max(0, Math.min(100, progress)) / 100 : 0));\n bucket.size += size;\n bucket.completed_bytes += completed;\n bucket.disk_bytes += completed;\n bucket.remaining_bytes += Math.max(0, size-completed);\n });\n bucket.progress_percent = bucket.size ? (bucket.completed_bytes / bucket.size) * 100 : 0;\n bucket.remaining_percent = Math.max(0, 100-bucket.progress_percent);\n return bucket;\n }\n function filterSummaryBucket(type){\n if(type==='moving') return {count:movingFilterCount()};\n if(activeTrackerFilter) return summarizeFilterRows(trackerScopedRows(), type);\n return torrentSummary?.filters?.[type] || {count:0};\n }\n function setFilterSummary(type){\n const el=$(FILTER_COUNT_IDS[type]);\n if(!el) return;\n const bucket=filterSummaryBucket(type);\n const meta=type==='moving' ? '' : filterMetaLine(bucket, type);\n const tooltip=type==='moving' && bucket.count ? 'Active moving operations' : filterTooltipLine(bucket, type);\n el.innerHTML=`${esc(bucket.count||0)}${meta?`${esc(meta)}`:''}`;\n const button=el.closest('.filter');\n if(button){\n const ariaLabel = tooltip ? `${button.dataset.filter || type}: ${tooltip.replace(/\\n/g, ', ')}` : '';\n button.classList.toggle('d-none', type==='moving' && !Number(bucket.count||0));\n setStableFilterTooltip(button, tooltip, ariaLabel);\n }\n }\n function labelNames(value){ return String(value||'').split(/[,;|]+/).map(x=>x.trim()).filter(Boolean).filter((x,i,a)=>a.indexOf(x)===i); }\n function labelValue(labels){ return [...new Set((labels||[]).map(x=>String(x||'').trim()).filter(Boolean))].join(', '); }\n function rowHasLabel(t,label){ return labelNames(t.label).includes(label); }\n function trackerRowsForHash(hash){ return trackerSummary.hashes?.[hash] || []; }\n function rowHasTracker(t, domain){ return trackerRowsForHash(t.hash).some(x=>x.domain===domain); }\n function torrentHasError(t){ return !!torrentErrorLog(t); }\n function isChecking(t){ return t?.status==='Checking' || Number(t?.hashing||0)>0; }\n function rowVisible(t){ const q=($('searchBox')?.value||'').toLowerCase(); if(q && !torrentSearchText(t).includes(q)) return false; if(activeTrackerFilter && !rowHasTracker(t, activeTrackerFilter)) return false; if(FILTER_COUNT_IDS[activeFilter]) return torrentMatchesFilterType(t, activeFilter); if(activeFilter.startsWith('label:')) return rowHasLabel(t,activeFilter.slice(6)); if(activeFilter.startsWith('smart:')) return smartViewVisible(t,activeFilter); return true; }\n function compareRows(a,b){\n const k=sortState.key;\n if(k==='eta'){\n // Note: ETA is displayed as text but sorted by eta_seconds; unavailable ETA stays last in both directions.\n const av=Number(a.eta_seconds||0), bv=Number(b.eta_seconds||0);\n const aMissing=!Number.isFinite(av)||av<=0, bMissing=!Number.isFinite(bv)||bv<=0;\n if(aMissing&&bMissing) return String(a.name||'').localeCompare(String(b.name||''));\n if(aMissing) return 1;\n if(bMissing) return -1;\n return (av>bv?1:avNumber(bv||0))?1:(Number(av||0)0?\" \":\" \"; }\n\n\n\n\n function updateSortHeaders(){\n document.querySelectorAll('.torrent-table thead th[data-sort]').forEach(th=>{\n const title = th.querySelector('.column-title');\n const base = th.dataset.baseText || (title ? title.textContent.trim() : th.textContent.trim());\n th.dataset.baseText = base;\n if(title) title.innerHTML = `${esc(base)}${sortIcon(th.dataset.sort)}`;\n else th.innerHTML = `${esc(base)}${sortIcon(th.dataset.sort)}`;\n th.classList.toggle('sorted', sortState.key === th.dataset.sort);\n });\n }\n // Note: Refreshes sidebar counters from the cached API summary, not from browser-side aggregation.\n function syncFilterButtons(){\n // Note: Tracker is a parent scope; regular filters stay active inside the selected tracker.\n document.querySelectorAll('.filter').forEach(x=>{\n const key=x.dataset.filter||'';\n if(key.startsWith('tracker:')) x.classList.toggle('active', activeTrackerFilter===key.slice(8));\n else if(x.dataset.trackerScope==='all') x.classList.toggle('active', !activeTrackerFilter);\n else x.classList.toggle('active', key===activeFilter);\n });\n }\n function renderCounts(){\n // Note: When the last move operation finishes, the hidden filter does not leave an empty list active.\n if(activeFilter==='moving' && !movingFilterCount()){ activeFilter='all'; mobileActiveFilterKey='all'; }\n syncFilterButtons();\n Object.keys(FILTER_COUNT_IDS).forEach(setFilterSummary);\n $('statSelected').textContent=selected.size;\n }\n function bindSidebarFilterClicks(root){\n root?.querySelectorAll('.filter').forEach(b=>b.addEventListener('click',()=>{\n const key=b.dataset.filter||'all';\n if(key.startsWith('tracker:')){ activeTrackerFilter=key.slice(8); mobileActiveFilterKey=key; }\n else if(b.dataset.trackerScope==='all'){ activeTrackerFilter=''; mobileActiveFilterKey='tracker:'; }\n else { activeTrackerFilter=''; activeFilter=key; mobileActiveFilterKey=key; }\n syncFilterButtons();\n saveActiveFilterPreference();\n if($('tableWrap')) $('tableWrap').scrollTop=0;\n if($('mobileList')) $('mobileList').scrollTop=0;\n scheduleRender(true);\n }));\n }\n function alwaysVisibleLabelSet(){\n // Note: Stalled and the configured Smart Queue marker stay outside the collapsed group because they are operational labels.\n return new Set([SMART_QUEUE_STALLED_LABEL, SMART_QUEUE_TECHNICAL_LABEL].filter(Boolean));\n }\n function labelFilterButton(l, counts){\n return ``;\n }\n function renderLabelFilters(force=false){\n const box=$('labelFilters');\n if(!box) return;\n const counts=new Map();\n trackerScopedRows().forEach(t=>labelNames(t.label).forEach(l=>counts.set(l,(counts.get(l)||0)+1)));\n const labels=[...counts.keys()].filter(l=>counts.get(l)>0).sort((a,b)=>a.localeCompare(b));\n if(activeFilter.startsWith('label:') && !counts.has(activeFilter.slice(6))){ activeFilter='all'; mobileActiveFilterKey='all'; }\n const sig=`${sidebarLabelsExpanded?1:0}|${SMART_QUEUE_TECHNICAL_LABEL}|${labels.map(l=>`${l}:${counts.get(l)}`).join('|')}`;\n if(!force && sig===lastLabelFiltersSignature){ syncFilterButtons(); return; }\n lastLabelFiltersSignature=sig;\n if(!labels.length){ box.innerHTML=''; return; }\n const alwaysVisible=alwaysVisibleLabelSet();\n const activeLabel=activeFilter.startsWith('label:') ? activeFilter.slice(6) : '';\n const pinned=labels.filter(l=>alwaysVisible.has(l) || l===activeLabel);\n const collapsible=labels.filter(l=>!pinned.includes(l));\n const hiddenCount=collapsible.length;\n const collapsed=!sidebarLabelsExpanded;\n const expandedLabels=collapsed ? [] : collapsible;\n const toggle=hiddenCount ? `` : '';\n box.innerHTML=`
Labels
${pinned.map(l=>labelFilterButton(l,counts)).join('')}${toggle}
${expandedLabels.map(l=>labelFilterButton(l,counts)).join('')}
`;\n bindSidebarFilterClicks(box);\n $('labelFiltersToggle')?.addEventListener('click',()=>{ sidebarLabelsExpanded=!sidebarLabelsExpanded; lastLabelFiltersSignature=''; saveSidebarStatePreference(); renderLabelFilters(true); });\n }\n function initSidebarShortcuts(){\n const box=$('shortcutList');\n const btn=$('shortcutToggle');\n if(!box || !btn) return;\n const apply=()=>{\n box.classList.toggle('is-collapsed', !sidebarShortcutsExpanded);\n btn.setAttribute('aria-expanded', sidebarShortcutsExpanded ? 'true' : 'false');\n btn.innerHTML=` ${sidebarShortcutsExpanded?'Hide shortcuts':'Show shortcuts'}${box.querySelectorAll('.shortcut').length}`;\n };\n apply();\n btn.addEventListener('click',()=>{ sidebarShortcutsExpanded=!sidebarShortcutsExpanded; apply(); saveSidebarStatePreference(); });\n }\n"; +export const torrentFilterUiSource = " function movingOperationRows(){\n // Note: The Moving filter is based only on active move operations, not queued jobs.\n return [...torrents.values()].filter(t=>{\n const op=activeOperationFor(t);\n return op?.action==='move' && op?.state==='running';\n });\n }\n function movingFilterCount(){ return movingOperationRows().length; }\n function torrentMatchesFilterType(t, type){\n if(type==='all') return true;\n if(type==='downloading') return !isChecking(t) && !t.complete && t.state && !t.paused && t.status!=='Queued';\n if(type==='queued') return !!t.queued || t.status==='Queued';\n if(type==='seeding') return !isChecking(t) && t.complete && t.state && !t.paused;\n if(type==='paused') return !!t.paused || t.status==='Paused';\n if(type==='checking') return isChecking(t);\n if(type==='error') return torrentHasError(t);\n if(type==='post_check') return t.status==='Post-check' || !!t.post_check;\n if(type==='stopped') return !t.state && !isChecking(t) && t.status!=='Post-check' && !t.post_check;\n if(type==='moving'){\n const op=activeOperationFor(t);\n return op?.action==='move' && op?.state==='running';\n }\n return true;\n }\n function trackerScopedRows(){\n const rows=[...torrents.values()];\n return activeTrackerFilter ? rows.filter(t=>rowHasTracker(t, activeTrackerFilter)) : rows;\n }\n function summarizeFilterRows(rows, type){\n const matched=rows.filter(t=>torrentMatchesFilterType(t, type));\n const bucket={count:matched.length,size:0,disk_bytes:0,completed_bytes:0,remaining_bytes:0};\n matched.forEach(t=>{\n const size=Number(t.size||0);\n const progress=Number(t.progress||0);\n const completed=Number(t.completed_bytes ?? t.completed ?? t.down_total ?? (size && Number.isFinite(progress) ? size * Math.max(0, Math.min(100, progress)) / 100 : 0));\n bucket.size += size;\n bucket.completed_bytes += completed;\n bucket.disk_bytes += completed;\n bucket.remaining_bytes += Math.max(0, size-completed);\n });\n bucket.progress_percent = bucket.size ? (bucket.completed_bytes / bucket.size) * 100 : 0;\n bucket.remaining_percent = Math.max(0, 100-bucket.progress_percent);\n return bucket;\n }\n function filterSummaryBucket(type){\n if(type==='moving') return {count:movingFilterCount()};\n if(activeTrackerFilter) return summarizeFilterRows(trackerScopedRows(), type);\n return torrentSummary?.filters?.[type] || {count:0};\n }\n function setFilterSummary(type){\n const el=$(FILTER_COUNT_IDS[type]);\n if(!el) return;\n const bucket=filterSummaryBucket(type);\n const meta=type==='moving' ? '' : filterMetaLine(bucket, type);\n const tooltip=type==='moving' && bucket.count ? 'Active moving operations' : filterTooltipLine(bucket, type);\n el.innerHTML=`${esc(bucket.count||0)}${meta?`${esc(meta)}`:''}`;\n const button=el.closest('.filter');\n if(button){\n const ariaLabel = tooltip ? `${button.dataset.filter || type}: ${tooltip.replace(/\\n/g, ', ')}` : '';\n button.classList.toggle('d-none', type==='moving' && !Number(bucket.count||0));\n setStableFilterTooltip(button, tooltip, ariaLabel);\n }\n }\n function labelNames(value){ return String(value||'').split(/[,;|]+/).map(x=>x.trim()).filter(Boolean).filter((x,i,a)=>a.indexOf(x)===i); }\n function labelValue(labels){ return [...new Set((labels||[]).map(x=>String(x||'').trim()).filter(Boolean))].join(', '); }\n function rowHasLabel(t,label){ return labelNames(t.label).includes(label); }\n function trackerRowsForHash(hash){ return trackerSummary.hashes?.[hash] || []; }\n function rowHasTracker(t, domain){ return trackerRowsForHash(t.hash).some(x=>x.domain===domain); }\n function torrentHasError(t){ return !!torrentErrorLog(t); }\n function isChecking(t){ return t?.status==='Checking' || Number(t?.hashing||0)>0; }\n function rowVisible(t){ const q=($('searchBox')?.value||'').toLowerCase(); if(q && !torrentSearchText(t).includes(q)) return false; if(activeTrackerFilter && !rowHasTracker(t, activeTrackerFilter)) return false; if(FILTER_COUNT_IDS[activeFilter]) return torrentMatchesFilterType(t, activeFilter); if(activeFilter.startsWith('label:')) return rowHasLabel(t,activeFilter.slice(6)); if(activeFilter.startsWith('smart:')) return smartViewVisible(t,activeFilter); return true; }\n function compareRows(a,b){\n const k=sortState.key;\n if(k==='eta'){\n // Note: ETA is displayed as text but sorted by eta_seconds; unavailable ETA stays last in both directions.\n const av=Number(a.eta_seconds||0), bv=Number(b.eta_seconds||0);\n const aMissing=!Number.isFinite(av)||av<=0, bMissing=!Number.isFinite(bv)||bv<=0;\n if(aMissing&&bMissing) return String(a.name||'').localeCompare(String(b.name||''));\n if(aMissing) return 1;\n if(bMissing) return -1;\n return (av>bv?1:avNumber(bv||0))?1:(Number(av||0)0?\" \":\" \"; }\n\n\n\n\n function updateSortHeaders(){\n document.querySelectorAll('.torrent-table thead th[data-sort]').forEach(th=>{\n const title = th.querySelector('.column-title');\n const base = th.dataset.baseText || (title ? title.textContent.trim() : th.textContent.trim());\n th.dataset.baseText = base;\n if(title) title.innerHTML = `${esc(base)}${sortIcon(th.dataset.sort)}`;\n else th.innerHTML = `${esc(base)}${sortIcon(th.dataset.sort)}`;\n th.classList.toggle('sorted', sortState.key === th.dataset.sort);\n });\n }\n // Note: Refreshes sidebar counters from the cached API summary, not from browser-side aggregation.\n function syncFilterButtons(){\n // Note: Tracker is a parent scope; regular filters stay active inside the selected tracker.\n document.querySelectorAll('.filter').forEach(x=>{\n const key=x.dataset.filter||'';\n if(key.startsWith('tracker:')) x.classList.toggle('active', activeTrackerFilter===key.slice(8));\n else if(x.dataset.trackerScope==='all') x.classList.toggle('active', !activeTrackerFilter);\n else x.classList.toggle('active', key===activeFilter);\n });\n }\n function renderCounts(){\n // Note: When the last move operation finishes, the hidden filter does not leave an empty list active.\n if(activeFilter==='moving' && !movingFilterCount()){ activeFilter='all'; mobileActiveFilterKey='all'; }\n syncFilterButtons();\n Object.keys(FILTER_COUNT_IDS).forEach(setFilterSummary);\n $('statSelected').textContent=selected.size;\n }\n function bindSidebarFilterClicks(root){\n root?.querySelectorAll('.filter').forEach(b=>b.addEventListener('click',()=>{\n const key=b.dataset.filter||'all';\n if(key.startsWith('tracker:')){ activeTrackerFilter=key.slice(8); mobileActiveFilterKey=key; }\n else if(b.dataset.trackerScope==='all'){ activeTrackerFilter=''; mobileActiveFilterKey='tracker:'; }\n else { activeTrackerFilter=''; activeFilter=key; mobileActiveFilterKey=key; }\n syncFilterButtons();\n saveActiveFilterPreference();\n if($('tableWrap')) $('tableWrap').scrollTop=0;\n if($('mobileList')) $('mobileList').scrollTop=0;\n scheduleRender(true);\n }));\n }\n function alwaysVisibleLabelSet(){\n // Note: Stalled and the configured Smart Queue marker stay outside the collapsed group because they are operational labels.\n return new Set([SMART_QUEUE_STALLED_LABEL, SMART_QUEUE_TECHNICAL_LABEL].filter(Boolean));\n }\n function labelFilterButton(l, counts){\n return ``;\n }\n function renderLabelFilters(force=false){\n const box=$('labelFilters');\n if(!box) return;\n const counts=new Map();\n trackerScopedRows().forEach(t=>labelNames(t.label).forEach(l=>counts.set(l,(counts.get(l)||0)+1)));\n const labels=[...counts.keys()].filter(l=>counts.get(l)>0).sort((a,b)=>a.localeCompare(b));\n if(activeFilter.startsWith('label:') && !counts.has(activeFilter.slice(6))){ activeFilter='all'; mobileActiveFilterKey='all'; }\n const sig=`${sidebarLabelsExpanded?1:0}|${SMART_QUEUE_TECHNICAL_LABEL}|${labels.map(l=>`${l}:${counts.get(l)}`).join('|')}`;\n if(!force && sig===lastLabelFiltersSignature){ syncFilterButtons(); return; }\n lastLabelFiltersSignature=sig;\n if(!labels.length){ box.innerHTML=''; return; }\n const alwaysVisible=alwaysVisibleLabelSet();\n const activeLabel=activeFilter.startsWith('label:') ? activeFilter.slice(6) : '';\n const pinned=labels.filter(l=>alwaysVisible.has(l) || l===activeLabel);\n const collapsible=labels.filter(l=>!pinned.includes(l));\n const hiddenCount=collapsible.length;\n const collapsed=!sidebarLabelsExpanded;\n const expandedLabels=collapsed ? [] : collapsible;\n const toggle=hiddenCount ? `` : '';\n box.innerHTML=`
Labels
${pinned.map(l=>labelFilterButton(l,counts)).join('')}${toggle}
${expandedLabels.map(l=>labelFilterButton(l,counts)).join('')}
`;\n bindSidebarFilterClicks(box);\n $('labelFiltersToggle')?.addEventListener('click',()=>{ sidebarLabelsExpanded=!sidebarLabelsExpanded; lastLabelFiltersSignature=''; saveSidebarStatePreference(); renderLabelFilters(true); });\n }\n function initSidebarShortcuts(){\n const box=$('shortcutList');\n const btn=$('shortcutToggle');\n if(!box || !btn) return;\n const apply=()=>{\n box.classList.toggle('is-collapsed', !sidebarShortcutsExpanded);\n btn.setAttribute('aria-expanded', sidebarShortcutsExpanded ? 'true' : 'false');\n btn.innerHTML=` ${sidebarShortcutsExpanded?'Hide shortcuts':'Show shortcuts'}${box.querySelectorAll('.shortcut').length}`;\n };\n apply();\n btn.addEventListener('click',()=>{ sidebarShortcutsExpanded=!sidebarShortcutsExpanded; apply(); saveSidebarStatePreference(); });\n }\n";