export const pathPickerToolsSource = " function copyText(text){\n text=String(text ?? '');\n if(navigator.clipboard && window.isSecureContext){\n return navigator.clipboard.writeText(text);\n }\n return new Promise((resolve,reject)=>{\n const ta=document.createElement('textarea');\n ta.value=text; ta.setAttribute('readonly','');\n ta.style.position='fixed'; ta.style.left='-9999px'; ta.style.top='0';\n document.body.appendChild(ta); ta.focus(); ta.select();\n try{ document.execCommand('copy') ? resolve() : reject(new Error('copy command failed')); }\n catch(e){ reject(e); }\n finally{ ta.remove(); }\n });\n }\n function copySelected(field){\n const t=torrents.get(selectedHash);\n if(!t) return toast('No torrent selected','warning');\n const value=String(t[field] ?? '');\n if(!value) return toast(`No ${field} to copy`,'warning');\n copyText(value).then(()=>toast(`Copied ${field}`,'success')).catch(()=>toast('Copy failed','danger'));\n }\n\n async function getDefaultDownloadPath(){ if(defaultDownloadPath) return defaultDownloadPath; try{ const j=await (await fetch('/api/path/default')).json(); if(j.ok && j.path) defaultDownloadPath=j.path; }catch(e){} return defaultDownloadPath || '/'; }\n async function applyDefaultDownloadPath(force=false){ const p=await getDefaultDownloadPath(); ['addPath','rssPath','autoEffectPath'].forEach(id=>{ const el=$(id); if(el && (force || !el.value)) el.value=p; }); return p; }\n async function openPathPicker(target){\n pathTarget=target;\n const modal=$('pathModal');\n if(!modal) return toastMessage('toast.pathPickerUnavailable','danger');\n const def=await getDefaultDownloadPath();\n const initial=def || ($(target)?.value||'/');\n // Note: The same modal is used for Move and simple path selection; only Move shows extra options.\n $('moveOptions')?.classList.toggle('d-none', target!=='move');\n if($('moveDataPhysical')) $('moveDataPhysical').checked=true;\n if($('moveRecheck')) $('moveRecheck').checked=true;\n resetInlineDirectoryCreate();\n // Note: The path picker can be opened from Add/Create modals, so it must sit above the parent modal.\n modal.classList.toggle('path-picker-stacked', document.querySelectorAll('.modal.show').length > 0);\n new bootstrap.Modal(modal).show();\n browsePath(initial);\n }\n function pathInfoHtml(j){\n // Note: Move modal shows remote-side capacity and entry counts before queuing a move.\n const meta=[];\n if(j.free_h) meta.push(` Free ${esc(j.free_h)}`);\n if(j.used_percent!==undefined) meta.push(`${esc(j.used_percent)}% used`);\n if(j.dir_count!==undefined) meta.push(`${esc(j.dir_count)} dirs`);\n if(j.file_count!==undefined) meta.push(`${esc(j.file_count)} files`);\n return meta.length ? `
${meta.join('')}
` : '';\n }\n function resetInlineDirectoryCreate(){\n const input=$('pathCreateName');\n if(input) input.value='';\n }\n function pathDirectoryRow(d){\n const disabled=!d.can_rename;\n const reason=d.rename_reason || (d.has_torrents?'Folder contains a known torrent path':(!d.empty?'Only empty folders can be renamed':'Rename folder'));\n // Note: Rename state comes from the backend so Add and Move share the same safety rules.\n return `
`;\n }\n async function browsePath(path){\n const list=$('pathList');\n const current=$('pathCurrent');\n if(!list || !current) return;\n list.innerHTML=' Loading...';\n try{\n const res=await fetch(`/api/path/browse?path=${encodeURIComponent(path||'/')}`);\n const j=await res.json();\n if(!j.ok) throw new Error(j.error);\n current.value=j.path;\n lastPathParent=j.parent;\n const rows=j.dirs.map(pathDirectoryRow).join('')||'
No directories.
';\n list.innerHTML=pathInfoHtml(j)+rows;\n }catch(e){\n list.innerHTML=`
${esc(e.message)}
`;\n }\n }\n function inlineDirectoryName(value){\n value=String(value||'').trim();\n if(!value || value==='.' || value==='..' || value.includes('/')) throw new Error('Enter a valid folder name');\n return value;\n }\n async function createInlineDirectory(){\n try{\n const parent=($('pathCurrent')?.value||'').trim();\n const name=inlineDirectoryName($('pathCreateName')?.value||'');\n // Note: Inline create avoids an extra modal and leaves the current Add/Move context untouched.\n await post('/api/path/directories',{parent,name});\n resetInlineDirectoryCreate();\n toast('Directory created','success');\n await browsePath(parent);\n }catch(e){ toast(e.message,'danger'); }\n }\n function startInlineRename(row){\n if(!row || row.dataset.canRename!=='1') return;\n const oldName=row.dataset.name||'';\n row.classList.add('is-renaming');\n row.innerHTML=`
`;\n const input=row.querySelector('.path-rename-input');\n input?.focus();\n input?.select();\n }\n async function submitInlineRename(row){\n try{\n const input=row?.querySelector('.path-rename-input');\n const newName=inlineDirectoryName(input?.value||'');\n const path=row?.dataset.path||'';\n if(newName===(row?.dataset.name||'')) return browsePath($('pathCurrent')?.value);\n // Note: Rename is limited by the backend to empty folders with no cached torrent path inside.\n await post('/api/path/directories/rename',{path,new_name:newName});\n toast('Directory renamed','success');\n await browsePath($('pathCurrent')?.value);\n }catch(e){ toast(e.message,'danger'); }\n }\n $('pathList')?.addEventListener('click',e=>{\n const rename=e.target.closest('.path-rename-btn');\n if(rename){ e.preventDefault(); e.stopPropagation(); startInlineRename(rename.closest('.path-row')); return; }\n const cancel=e.target.closest('.path-rename-cancel');\n if(cancel){ e.preventDefault(); browsePath($('pathCurrent')?.value); return; }\n const open=e.target.closest('.path-row-open');\n if(open){ const r=open.closest('.path-row'); if(r) browsePath(r.dataset.path); }\n });\n $('pathList')?.addEventListener('submit',e=>{ const form=e.target.closest('.path-rename-form'); if(form){ e.preventDefault(); submitInlineRename(form.closest('.path-row')); } });\n $('pathCreateBtn')?.addEventListener('click',createInlineDirectory);\n $('pathCreateName')?.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); createInlineDirectory(); } });\n $('pathGoBtn')?.addEventListener('click',()=>browsePath($('pathCurrent')?.value));\n $('pathUpBtn')?.addEventListener('click',()=>browsePath(lastPathParent));\n $('pathReloadBtn')?.addEventListener('click',()=>browsePath($('pathCurrent')?.value));\n $('pathSelectBtn')?.addEventListener('click',async()=>{\n const p=($('pathCurrent')?.value||'').trim();\n if(!p) return toastMessage('toast.pathEmpty','warning');\n if(pathTarget==='move'){\n const hashes=selectedHashes();\n if(!hashes.length) return toastMessage('toast.noTorrentsSelected','warning');\n const j=await post('/api/torrents/move',{hashes,path:p,move_data:!!($('moveDataPhysical')?.checked),recheck:!!($('moveRecheck')?.checked)});\n markQueuedJobs(j,hashes,'move');\n const parts=Number(j.bulk_parts||1);\n toastMessage('toast.moveQueued','success',{parts,physical:$('moveDataPhysical')?.checked});\n } else if($(pathTarget)) {\n $(pathTarget).value=p;\n }\n bootstrap.Modal.getInstance($('pathModal'))?.hide();\n });\n document.querySelectorAll('.browse-path').forEach(b=>b.addEventListener('click',()=>openPathPicker(b.dataset.target)));\n";