Files
pyTorrent/pytorrent/static/js/torrentChunkDetails.js
T
Mateusz Gruszczyński 1068aba11c splij all js
2026-05-31 13:30:32 +02:00

2 lines
7.6 KiB
JavaScript

export const torrentChunkDetailsSource = " const CHUNK_DENSITY_OPTIONS = {\n compact: {label: 'Compact', maxCells: 2400},\n normal: {label: 'Normal', maxCells: 1400},\n detailed: {label: 'Detailed', maxCells: 700},\n };\n const CHUNK_FILTER_OPTIONS = [\n ['all', 'All'],\n ['problem', 'Missing + partial'],\n ['missing', 'Missing'],\n ['partial', 'Partial'],\n ['seen', 'Seen by peers'],\n ['complete', 'Complete'],\n ];\n let chunkFilterMode = localStorage.getItem('chunkFilterMode') || 'all';\n let chunkDensityMode = localStorage.getItem('chunkDensityMode') || 'normal';\n let lastChunkData = null;\n\n function chunkMaxCellsForDensity(){\n // Note: Density changes the API grouping level and the CSS cell size together.\n return CHUNK_DENSITY_OPTIONS[chunkDensityMode]?.maxCells || CHUNK_DENSITY_OPTIONS.normal.maxCells;\n }\n function chunkCellsForFilter(cells){\n const list = Array.isArray(cells) ? cells : [];\n if(chunkFilterMode === 'all') return list;\n if(chunkFilterMode === 'problem') return list.filter(cell => ['missing','partial'].includes(cell.status));\n return list.filter(cell => cell.status === chunkFilterMode);\n }\n function chunkStatusLabel(status){\n return ({complete:'Complete', partial:'Partial', missing:'Missing', seen:'Seen by peers'}[status] || 'Unknown');\n }\n function chunkCellTitle(cell){\n const first = cell.first_chunk ?? '-';\n const last = cell.last_chunk ?? first;\n const pct = Number(cell.percent||0).toFixed(1).replace(/\\.0$/,'');\n const completed = Number(cell.completed ?? 0);\n const total = Number(cell.total ?? cell.unit_count ?? 1);\n const grouped = cell.grouped ? `Grouped visual cell: ${cell.unit_count || 1} piece(s)` : 'Single piece';\n return [\n `Pieces: ${first}-${last}`,\n `Status: ${chunkStatusLabel(cell.status)}`,\n `Progress: ${pct}%`,\n `Complete pieces: ${completed}/${total}`,\n grouped,\n ].join(' | ');\n }\n function chunkCellMarkup(cell){\n const pct = Math.max(0, Math.min(100, Number(cell.percent || 0)));\n const cls = `chunk-cell chunk-${esc(cell.status || 'missing')}${cell.grouped ? ' is-grouped' : ''}`;\n return `<button class=\"${cls}\" type=\"button\" data-first-chunk=\"${esc(cell.first_chunk)}\" data-last-chunk=\"${esc(cell.last_chunk)}\" title=\"${esc(chunkCellTitle(cell))}\" aria-label=\"${esc(chunkCellTitle(cell))}\"><span style=\"height:${pct}%\"></span></button>`;\n }\n function renderChunkLegend(summary){\n const items=[['complete','Complete'],['partial','Partial'],['missing','Missing'],['seen','Seen by peers']];\n return items.map(([key,label])=>`<span class=\"chunk-legend-item\"><i class=\"chunk-dot chunk-${key}\"></i>${label} <b>${esc(summary?.[key]??0)}</b></span>`).join('');\n }\n function renderChunkControls(){\n const filters = CHUNK_FILTER_OPTIONS.map(([value,label]) => `<option value=\"${esc(value)}\" ${chunkFilterMode === value ? 'selected' : ''}>${esc(label)}</option>`).join('');\n const densities = Object.entries(CHUNK_DENSITY_OPTIONS).map(([value,cfg]) => `<option value=\"${esc(value)}\" ${chunkDensityMode === value ? 'selected' : ''}>${esc(cfg.label)}</option>`).join('');\n return `<div class=\"chunk-controls\"><label><span>Filter</span><select id=\"chunkFilterMode\" class=\"form-select form-select-sm\">${filters}</select></label><label><span>Density</span><select id=\"chunkDensityMode\" class=\"form-select form-select-sm\">${densities}</select></label></div>`;\n }\n function selectedChunkRange(){\n const selected=[...document.querySelectorAll('#detailPane .chunk-cell.is-selected')].map(el=>({first:Number(el.dataset.firstChunk||0),last:Number(el.dataset.lastChunk||0)}));\n if(!selected.length) return null;\n return {first_chunk:Math.min(...selected.map(x=>x.first)),last_chunk:Math.max(...selected.map(x=>x.last)),count:selected.length};\n }\n function updateChunkSelectionInfo(){\n const info=$('chunkSelectionInfo');\n if(!info) return;\n const range=selectedChunkRange();\n const filteredCount=document.querySelectorAll('#detailPane .chunk-cell').length;\n const totalCount=lastChunkData?.cells?.length || 0;\n if(range){\n info.textContent=`Selected ${range.count} cell(s), pieces ${range.first_chunk}-${range.last_chunk}.`;\n return;\n }\n const filterText=chunkFilterMode === 'all' ? '' : ` Showing ${filteredCount}/${totalCount} cell(s).`;\n info.textContent=`Select one or more visual cells to prioritize files that overlap that range.${filterText}`;\n }\n function renderChunks(data){\n const pane=$('detailPane');\n const chunks=data||{};\n lastChunkData=chunks;\n const allCells=chunks.cells||[];\n const cells=chunkCellsForFilter(allCells);\n const grouped=chunks.grouped?'<span class=\"badge text-bg-warning\">grouped for performance</span>':'';\n const meta=[\n ['Piece size', chunks.chunk_size_h || '-'],\n ['Pieces', chunks.size_chunks ?? 0],\n ['Complete pieces', chunks.completed_chunks ?? 0],\n ['Hashed pieces', chunks.chunks_hashed ?? 0],\n ['Visual cells', chunks.visual_cells ?? allCells.length],\n ].map(([label,value])=>`<div class=\"chunk-stat\"><b>${esc(label)}</b><span>${esc(value)}</span></div>`).join('');\n pane.innerHTML=`\n <div class=\"chunks-panel\">\n <div class=\"chunks-toolbar\">\n <div class=\"chunks-title\"><i class=\"fa-solid fa-grip\"></i> Chunks ${grouped}</div>\n <div class=\"chunks-actions\">\n <button class=\"btn btn-sm btn-outline-primary chunk-action-recheck\" type=\"button\"><i class=\"fa-solid fa-shield-halved\"></i> Recheck torrent</button>\n <button class=\"btn btn-sm btn-outline-success chunk-action-prioritize\" type=\"button\"><i class=\"fa-solid fa-arrow-up\"></i> High priority files for selection</button>\n <button class=\"btn btn-sm btn-outline-secondary chunk-refresh\" type=\"button\"><i class=\"fa-solid fa-rotate\"></i> Refresh</button>\n </div>\n </div>\n <div class=\"chunk-stats\">${meta}</div>\n <div class=\"chunk-tools-row\">\n <div class=\"chunk-legend\">${renderChunkLegend(chunks.summary||{})}</div>\n ${renderChunkControls()}\n </div>\n <div id=\"chunkSelectionInfo\" class=\"chunk-selection-info\"></div>\n <div class=\"chunk-grid\" data-density=\"${esc(chunkDensityMode)}\" style=\"--chunk-count:${Math.max(1, Math.min(96, cells.length || 1))}\">${cells.map(chunkCellMarkup).join('') || '<div class=\"empty\">No chunk cells for this filter.</div>'}</div>\n </div>`;\n updateChunkSelectionInfo();\n }\n async function runChunkAction(action,payload={}){\n if(!selectedHash) return toastMessage('toast.noTorrentSelected','warning');\n setBusy(true);\n try{\n const j=await post(`/api/torrents/${encodeURIComponent(selectedHash)}/chunks/${action}`,payload);\n toast(j.message || appMessage('toast.chunkActionDone',{action}),'success');\n await loadDetails('chunks');\n }catch(e){ toast(e.message,'danger'); }\n finally{ setBusy(false); }\n }\n document.addEventListener('change', e=>{\n const filter=e.target.closest('#chunkFilterMode');\n if(filter){\n chunkFilterMode=filter.value || 'all';\n localStorage.setItem('chunkFilterMode', chunkFilterMode);\n if(lastChunkData && activeTab()==='chunks') renderChunks(lastChunkData);\n return;\n }\n const density=e.target.closest('#chunkDensityMode');\n if(density){\n chunkDensityMode=density.value || 'normal';\n localStorage.setItem('chunkDensityMode', chunkDensityMode);\n if(activeTab()==='chunks') loadDetails('chunks');\n }\n });\n";