From d48c3331c6bd3dc0183add0e03a88f916355b7f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Jun 2026 18:27:36 +0200 Subject: [PATCH] clear --- pytorrent/static/js/createTorrent.js | 2 +- pytorrent/static/js/torrentAdd.js | 2 +- pytorrent/static/styles.css | 5 +++++ pytorrent/templates/index.html | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pytorrent/static/js/createTorrent.js b/pytorrent/static/js/createTorrent.js index 3bf3e50..64125ad 100644 --- a/pytorrent/static/js/createTorrent.js +++ b/pytorrent/static/js/createTorrent.js @@ -1 +1 @@ -export const createTorrentSource = " function isCreateTorrentTabActive(){\n return $('createTorrentPane')?.classList.contains('active');\n }\n function syncAddAndCreateActions(){\n const createActive = isCreateTorrentTabActive();\n $('addBtn')?.classList.toggle('d-none', !!createActive);\n $('createTorrentBtn')?.classList.toggle('d-none', !createActive);\n }\n function createTorrentPayload(){\n const fd = new FormData();\n fd.append('source_path', $('createSourcePath')?.value || '');\n fd.append('trackers', $('createTrackers')?.value || '');\n fd.append('comment', $('createComment')?.value || '');\n fd.append('source', $('createSourceName')?.value || '');\n fd.append('piece_size_kib', $('createPieceSize')?.value || '256');\n fd.append('private', $('createPrivate')?.checked ? '1' : '0');\n fd.append('share', $('createShare')?.checked ? '1' : '0');\n fd.append('label', $('createLabel')?.value || '');\n return fd;\n }\n function downloadCreatedTorrent(blob,name){\n const obj = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = obj;\n a.download = name;\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(()=>URL.revokeObjectURL(obj), 1000);\n }\n async function createTorrentFromModal(){\n const btn = $('createTorrentBtn');\n const info = $('createTorrentInfo');\n buttonBusy(btn, true);\n setBusy(true, 'Creating torrent...');\n if(info) info.textContent = 'Creating .torrent file...';\n try{\n const res = await fetch('/api/torrents/create', {method: 'POST', body: createTorrentPayload()});\n if(!res.ok){\n const j = await res.json().catch(()=>({}));\n throw new Error(j.error || `Create failed (${res.status})`);\n }\n const name = filenameFromResponse(res, 'created.torrent');\n const message = res.headers.get('X-PyTorrent-Create-Message') || 'Torrent created';\n const blob = await res.blob();\n downloadCreatedTorrent(blob, name);\n if(info) info.textContent = message;\n toast(message, 'success');\n }catch(e){\n if(info) info.textContent = e.message;\n toast(e.message, 'danger');\n }finally{\n setBusy(false);\n buttonBusy(btn, false);\n }\n }\n $('addModal')?.addEventListener('shown.bs.modal', syncAddAndCreateActions);\n document.querySelectorAll('#addModal [data-bs-toggle=\"pill\"]').forEach(tab => tab.addEventListener('shown.bs.tab', syncAddAndCreateActions));\n $('createTorrentBtn')?.addEventListener('click', createTorrentFromModal);\n"; +export const createTorrentSource = " function isCreateTorrentTabActive(){\n return $('createTorrentPane')?.classList.contains('active');\n }\n function syncAddAndCreateActions(){\n // Note: Keeps footer actions scoped to the currently selected Add/Create tab.\n const createActive = isCreateTorrentTabActive();\n $('addBtn')?.classList.toggle('d-none', !!createActive);\n $('clearAddTorrentBtn')?.classList.toggle('d-none', !!createActive);\n $('createTorrentBtn')?.classList.toggle('d-none', !createActive);\n }\n function createTorrentPayload(){\n const fd = new FormData();\n fd.append('source_path', $('createSourcePath')?.value || '');\n fd.append('trackers', $('createTrackers')?.value || '');\n fd.append('comment', $('createComment')?.value || '');\n fd.append('source', $('createSourceName')?.value || '');\n fd.append('piece_size_kib', $('createPieceSize')?.value || '256');\n fd.append('private', $('createPrivate')?.checked ? '1' : '0');\n fd.append('share', $('createShare')?.checked ? '1' : '0');\n fd.append('label', $('createLabel')?.value || '');\n return fd;\n }\n function downloadCreatedTorrent(blob,name){\n const obj = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = obj;\n a.download = name;\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(()=>URL.revokeObjectURL(obj), 1000);\n }\n async function createTorrentFromModal(){\n const btn = $('createTorrentBtn');\n const info = $('createTorrentInfo');\n buttonBusy(btn, true);\n setBusy(true, 'Creating torrent...');\n if(info) info.textContent = 'Creating .torrent file...';\n try{\n const res = await fetch('/api/torrents/create', {method: 'POST', body: createTorrentPayload()});\n if(!res.ok){\n const j = await res.json().catch(()=>({}));\n throw new Error(j.error || `Create failed (${res.status})`);\n }\n const name = filenameFromResponse(res, 'created.torrent');\n const message = res.headers.get('X-PyTorrent-Create-Message') || 'Torrent created';\n const blob = await res.blob();\n downloadCreatedTorrent(blob, name);\n if(info) info.textContent = message;\n toast(message, 'success');\n }catch(e){\n if(info) info.textContent = e.message;\n toast(e.message, 'danger');\n }finally{\n setBusy(false);\n buttonBusy(btn, false);\n }\n }\n $('addModal')?.addEventListener('shown.bs.modal', syncAddAndCreateActions);\n document.querySelectorAll('#addModal [data-bs-toggle=\"pill\"]').forEach(tab => tab.addEventListener('shown.bs.tab', syncAddAndCreateActions));\n $('createTorrentBtn')?.addEventListener('click', createTorrentFromModal);\n"; diff --git a/pytorrent/static/js/torrentAdd.js b/pytorrent/static/js/torrentAdd.js index 4547179..ae1c784 100644 --- a/pytorrent/static/js/torrentAdd.js +++ b/pytorrent/static/js/torrentAdd.js @@ -1 +1 @@ -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)=>`${esc(f.path)}${esc(fmtBytes(f.size||0))}`).join('');\n const limitWarn=item.xmlrpc_too_large?`
Too large for current rTorrent XML-RPC upload limit: request ${esc(item.xmlrpc_request_h||'')} exceeds the configured limit. Change network.xmlrpc.size_limit in rTorrent config, e.g. 16M.
`:'';\n return `
${esc(item.name||item.filename)}${item.duplicate?'duplicate':''}${esc(fmtBytes(item.size||0))} · ${esc(item.file_count||0)} files
${esc(item.info_hash||'')}
${limitWarn}
${files}
`;\n }).join('');\n box.innerHTML=`
Preview before adding
${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=`
${esc(e.message)}
`; }\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"; +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)=>`${esc(f.path)}${esc(fmtBytes(f.size||0))}`).join('');\n const limitWarn=item.xmlrpc_too_large?`
Too large for current rTorrent XML-RPC upload limit: request ${esc(item.xmlrpc_request_h||'')} exceeds the configured limit. Change network.xmlrpc.size_limit in rTorrent config, e.g. 16M.
`:'';\n return `
${esc(item.name||item.filename)}${item.duplicate?'duplicate':''}${esc(fmtBytes(item.size||0))} · ${esc(item.file_count||0)} files
${esc(item.info_hash||'')}
${limitWarn}
${files}
`;\n }).join('');\n box.innerHTML=`
Preview before adding
${cards}`;\n }\n async function previewTorrentFiles(){\n const input=$('torrentFiles');\n const files=[...(input?.files||[])];\n const info=$('torrentFilesInfo');\n if(info) info.textContent=files.length?`Selected files: ${files.length}`:'No files selected.';\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=`
${esc(e.message)}
`; }\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 resetAddTorrentFileInput(){\n // Note: Keeps file input, summary and preview synchronized after clearing or removing selected torrents.\n if($('torrentFiles')) $('torrentFiles').value='';\n if($('torrentFilesInfo')) $('torrentFilesInfo').textContent='No files selected.';\n renderTorrentPreview([]);\n }\n function resetAddTorrentForm(){\n // Note: Clears only the Add tab fields and preserves the Create Torrent tab state.\n if($('magnetInput')) $('magnetInput').value='';\n if($('addPath')) $('addPath').value='';\n if($('addLabel')) $('addLabel').value='';\n if($('addStart')) $('addStart').checked=true;\n resetAddTorrentFileInput();\n }\n function removeTorrentPreviewItem(card){\n // Note: Removes one selected torrent file from the Add tab without resetting other unfinished form data.\n const input=$('torrentFiles');\n const filename=card?.dataset?.filename || '';\n if(input?.files?.length && filename && typeof DataTransfer !== 'undefined'){\n const nextFiles=new DataTransfer();\n [...input.files].forEach(file=>{\n if(file.name!==filename) nextFiles.items.add(file);\n });\n input.files=nextFiles.files;\n previewTorrentFiles();\n return;\n }\n const key=card?.dataset?.torrent || '';\n renderTorrentPreview(addPreviewState.items.filter(item=>(item.info_hash||item.filename)!==key));\n }\n function clearAddTorrentFormFromModal(){\n // Note: Manual clear action is separate from closing the modal, so draft data still survives normal close/open cycles.\n resetAddTorrentForm();\n toast('Add torrent form cleared.', 'success');\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);\n $('clearAddTorrentBtn')?.addEventListener('click',clearAddTorrentFormFromModal);\n $('torrentFiles')?.addEventListener('change',previewTorrentFiles);\n $('torrentPreview')?.addEventListener('click',e=>{\n const card=e.target.closest('.torrent-preview-card');\n if(!card) return;\n if(e.target.closest('.preview-select-all')) card.querySelectorAll('.preview-file-priority').forEach(x=>x.checked=true);\n if(e.target.closest('.preview-select-none')) card.querySelectorAll('.preview-file-priority').forEach(x=>x.checked=false);\n if(e.target.closest('.preview-remove-torrent')) removeTorrentPreviewItem(card);\n });\n"; diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css index 22b17b9..2c6bfe6 100644 --- a/pytorrent/static/styles.css +++ b/pytorrent/static/styles.css @@ -3969,6 +3969,11 @@ body, margin-top: 0.25rem; } +.add-modal-footer { + align-items: center; + gap: 0.5rem; +} + .add-torrent-layout { display: grid; gap: 0.85rem; diff --git a/pytorrent/templates/index.html b/pytorrent/templates/index.html index d134c99..7136b34 100644 --- a/pytorrent/templates/index.html +++ b/pytorrent/templates/index.html @@ -273,7 +273,7 @@ - +