Files
pyTorrent/pytorrent/static/js/torrentAdd.js
Mateusz Gruszczyński 32c780793b fix in js's
2026-05-26 09:07:34 +02:00

2 lines
9.4 KiB
JavaScript

export const torrentAddSource = " // Note: Add Torrent modal preview, submission and drag/drop upload handling are grouped here.\n const addPreviewState = {items: []};\n function renderTorrentPreview(items=[]){\n addPreviewState.items = items;\n const box=$('torrentPreview');\n if(!box) return;\n if(!items.length){ box.innerHTML=''; return; }\n const cards=items.map(item=>{\n const files=(item.files||[]).map((f,index)=>`<tr><td><input class=\"preview-file-priority\" type=\"checkbox\" data-torrent=\"${esc(item.info_hash||item.filename)}\" data-index=\"${index}\" checked></td><td>${esc(f.path)}</td><td>${esc(fmtBytes(f.size||0))}</td></tr>`).join('');\n const limitWarn=item.xmlrpc_too_large?`<div class=\"alert alert-danger py-1 px-2 mt-2 mb-0 small\"><i class=\"fa-solid fa-triangle-exclamation\"></i> Too large for current rTorrent XML-RPC upload limit: request ${esc(item.xmlrpc_request_h||'')} exceeds the configured limit. Change <b>network.xmlrpc.size_limit</b> in rTorrent config, e.g. 16M.</div>`:'';\n return `<div class=\"torrent-preview-card ${item.duplicate?'is-duplicate':''}\" data-torrent=\"${esc(item.info_hash||item.filename)}\"><div class=\"torrent-preview-head\"><b>${esc(item.name||item.filename)}</b>${item.duplicate?'<span class=\"badge text-bg-danger\">duplicate</span>':''}<span class=\"text-muted\">${esc(fmtBytes(item.size||0))} · ${esc(item.file_count||0)} files</span></div><div class=\"small text-muted\">${esc(item.info_hash||'')}</div>${limitWarn}<div class=\"preview-actions\"><button type=\"button\" class=\"btn btn-xs btn-outline-secondary preview-select-all\"><i class=\"fa-solid fa-check-double\"></i> All</button><button type=\"button\" class=\"btn btn-xs btn-outline-secondary preview-select-none\"><i class=\"fa-solid fa-ban\"></i> None</button></div><div class=\"table-responsive\"><table class=\"table table-sm preview-file-table\"><tbody>${files}</tbody></table></div></div>`;\n }).join('');\n box.innerHTML=`<div class=\"torrent-preview-title\">Preview before adding</div>${cards}`;\n }\n async function previewTorrentFiles(){\n const input=$('torrentFiles');\n const files=[...(input?.files||[])];\n $('torrentFilesInfo').textContent=files.length?`Selected files: ${files.length}`:'You can select multiple files at once.';\n if(!files.length) return renderTorrentPreview([]);\n const fd=new FormData();\n files.forEach(f=>fd.append('files',f));\n try{\n const j=await (await fetch('/api/torrents/preview',{method:'POST',body:fd})).json();\n if(!j.ok) throw new Error(j.error||'Preview failed');\n renderTorrentPreview(j.previews||[]);\n }catch(e){ if($('torrentPreview')) $('torrentPreview').innerHTML=`<div class=\"text-danger\">${esc(e.message)}</div>`; }\n }\n function collectPreviewPriorities(){\n const out={};\n addPreviewState.items.forEach(item=>{\n const key=item.info_hash||item.filename;\n out[key]=[...(item.files||[])].map((f,index)=>({index,priority:document.querySelector(`.preview-file-priority[data-torrent=\"${CSS.escape(key)}\"][data-index=\"${index}\"]`)?.checked ? 1 : 0}));\n });\n return out;\n }\n function torrentFilesFromDrop(event){\n return [...(event.dataTransfer?.files||[])].filter(file=>/\\.torrent$/i.test(file.name||'') || file.type==='application/x-bittorrent');\n }\n function dragHasFiles(event){\n const dt=event.dataTransfer;\n if(!dt) return false;\n if([...(dt.types||[])].includes('Files')) return true;\n return [...(dt.items||[])].some(item=>item.kind==='file');\n }\n async function droppedTorrentSummary(files){\n const fd=new FormData();\n files.forEach(file=>fd.append('files',file));\n try{\n const j=await (await fetch('/api/torrents/preview',{method:'POST',body:fd})).json();\n if(!j.ok) throw new Error(j.error||'Preview failed');\n const names=(j.previews||[]).map(item=>`${item.duplicate?'[duplicate] ':''}${item.name||item.filename}`).filter(Boolean);\n return names.length ? names : files.map(file=>file.name);\n }catch(e){\n return files.map(file=>file.name);\n }\n }\n async function addDroppedTorrentFiles(files){\n const torrentFiles=[...files].filter(file=>/\\.torrent$/i.test(file.name||'') || file.type==='application/x-bittorrent');\n if(!torrentFiles.length){ toastMessage('toast.dropOnlyTorrents','warning'); return; }\n const names=await droppedTorrentSummary(torrentFiles);\n const preview=names.slice(0,8).join('\\n');\n const suffix=names.length>8?`\\n...and ${names.length-8} more`:'';\n if(!confirm(`Add ${torrentFiles.length} torrent file(s)?\\n\\n${preview}${suffix}`)) return;\n setBusy(true,'Adding dropped torrent files...');\n try{\n const fd=new FormData();\n fd.append('uris','');\n fd.append('directory',await getDefaultDownloadPath());\n fd.append('label','');\n fd.append('start','1');\n torrentFiles.forEach(file=>fd.append('files',file));\n const res=await fetch('/api/torrents/add',{method:'POST',body:fd});\n const j=await res.json().catch(()=>({ok:false,error:`Add failed: HTTP ${res.status}`}));\n if(!res.ok || !j.ok) throw new Error(j.error||`Add failed: HTTP ${res.status}`);\n const skipped=(j.skipped_duplicates||[]).length;\n const queued=(j.job_ids||[]).length;\n if(queued && skipped) toastMessage('toast.droppedAddedSkipped','warning',{queued, skipped});\n else if(queued) toastMessage('toast.droppedAdded','success',{queued});\n else if(skipped) toastMessage('toast.droppedSkipped','warning',{skipped});\n else toastMessage('toast.droppedNone','warning');\n }catch(e){\n toast(e.message,'danger');\n }finally{\n setBusy(false);\n document.body.classList.remove('dragging-torrent-files');\n }\n }\n function setupTorrentDropZone(){\n const zones=[$('tableWrap'),$('torrentBody'),$('mobileList'),document.querySelector('.content'),document.body].filter(Boolean);\n let dragDepth=0;\n const markActive=()=>document.body.classList.add('dragging-torrent-files');\n const clearActive=()=>document.body.classList.remove('dragging-torrent-files');\n const onDragEnter=event=>{\n if(!dragHasFiles(event)) return;\n event.preventDefault();\n dragDepth+=1;\n markActive();\n };\n const onDragOver=event=>{\n if(!dragHasFiles(event)) return;\n event.preventDefault();\n if(event.dataTransfer) event.dataTransfer.dropEffect='copy';\n markActive();\n };\n const onDragLeave=event=>{\n if(!dragHasFiles(event)) return;\n dragDepth=Math.max(0,dragDepth-1);\n if(!dragDepth) clearActive();\n };\n const onDrop=event=>{\n if(!dragHasFiles(event)) return;\n event.preventDefault();\n event.stopPropagation();\n dragDepth=0;\n clearActive();\n addDroppedTorrentFiles(event.dataTransfer?.files||[]);\n };\n zones.forEach(zone=>{\n if(zone.dataset?.torrentDropZoneBound==='1') return;\n if(zone.dataset) zone.dataset.torrentDropZoneBound='1';\n zone.addEventListener('dragenter',onDragEnter);\n zone.addEventListener('dragover',onDragOver);\n zone.addEventListener('dragleave',onDragLeave);\n zone.addEventListener('drop',onDrop);\n });\n }\n function hasTooLargeTorrentPreview(){\n // Note: Client-side upload blocking mirrors the server validation and gives feedback before the add request.\n return addPreviewState.items.some(item=>item.xmlrpc_too_large);\n }\n function addTorrentPayload(){\n const fd=new FormData();\n fd.append('uris',$('magnetInput')?.value||'');\n fd.append('directory',$('addPath')?.value||'');\n fd.append('label',$('addLabel')?.value||'');\n fd.append('start',$('addStart')?.checked?'1':'0');\n fd.append('file_priorities',JSON.stringify(collectPreviewPriorities()));\n [...($('torrentFiles')?.files||[])].forEach(f=>fd.append('files',f));\n return fd;\n }\n function resetAddTorrentForm(){\n if($('magnetInput')) $('magnetInput').value='';\n if($('torrentFiles')) $('torrentFiles').value='';\n renderTorrentPreview([]);\n }\n async function addTorrentFromModal(){\n const btn=$('addBtn');\n buttonBusy(btn,true);\n setBusy(true);\n try{\n if(hasTooLargeTorrentPreview()) throw new Error(appMessage('toast.addTooLarge'));\n const res=await fetch('/api/torrents/add',{method:'POST',body:addTorrentPayload()});\n const j=await res.json().catch(()=>({ok:false,error:`Add failed: HTTP ${res.status}`}));\n if(!res.ok || !j.ok) throw new Error(j.error||`Add failed: HTTP ${res.status}`);\n const skipped=(j.skipped_duplicates||[]).length;\n if(skipped) toastMessage('toast.addQueuedSkipped','warning',{count:skipped});\n else toastMessage('toast.addQueued','success');\n resetAddTorrentForm();\n bootstrap.Modal.getInstance($('addModal'))?.hide();\n }catch(e){\n toast(e.message,'danger');\n }finally{\n buttonBusy(btn,false);\n setBusy(false);\n }\n }\n $('addBtn')?.addEventListener('click',addTorrentFromModal); $('torrentFiles')?.addEventListener('change',previewTorrentFiles); $('torrentPreview')?.addEventListener('click',e=>{const card=e.target.closest('.torrent-preview-card'); if(!card) return; if(e.target.closest('.preview-select-all')) card.querySelectorAll('.preview-file-priority').forEach(x=>x.checked=true); if(e.target.closest('.preview-select-none')) card.querySelectorAll('.preview-file-priority').forEach(x=>x.checked=false);});\n";