Files
decypharr/pkg/web/assets/build/js/dashboard.js
Mukhtar Akere afe577bf2f - Fix repair bugs
- Minor html/js bugs from new template
- Other minor issues
2025-07-13 06:30:02 +01:00

1 line
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class TorrentDashboard{constructor(){this.state={torrents:[],selectedTorrents:new Set,categories:new Set,filteredTorrents:[],selectedCategory:"",selectedState:"",sortBy:"added_on",itemsPerPage:20,currentPage:1,selectedTorrentContextMenu:null},this.refs={torrentsList:document.getElementById("torrentsList"),categoryFilter:document.getElementById("categoryFilter"),stateFilter:document.getElementById("stateFilter"),sortSelector:document.getElementById("sortSelector"),selectAll:document.getElementById("selectAll"),batchDeleteBtn:document.getElementById("batchDeleteBtn"),batchDeleteDebridBtn:document.getElementById("batchDeleteDebridBtn"),refreshBtn:document.getElementById("refreshBtn"),torrentContextMenu:document.getElementById("torrentContextMenu"),paginationControls:document.getElementById("paginationControls"),paginationInfo:document.getElementById("paginationInfo"),emptyState:document.getElementById("emptyState")},this.init()}init(){this.bindEvents(),this.loadTorrents(),this.startAutoRefresh()}bindEvents(){this.refs.refreshBtn.addEventListener("click",()=>this.loadTorrents()),this.refs.batchDeleteBtn.addEventListener("click",()=>this.deleteSelectedTorrents()),this.refs.batchDeleteDebridBtn.addEventListener("click",()=>this.deleteSelectedTorrents(!0)),this.refs.selectAll.addEventListener("change",t=>this.toggleSelectAll(t.target.checked)),this.refs.categoryFilter.addEventListener("change",t=>this.setFilter("category",t.target.value)),this.refs.stateFilter.addEventListener("change",t=>this.setFilter("state",t.target.value)),this.refs.sortSelector.addEventListener("change",t=>this.setSort(t.target.value)),this.bindContextMenu(),this.refs.torrentsList.addEventListener("change",t=>{t.target.classList.contains("torrent-select")&&this.toggleTorrentSelection(t.target.dataset.hash,t.target.checked)})}bindContextMenu(){this.refs.torrentsList.addEventListener("contextmenu",t=>{const e=t.target.closest("tr[data-hash]");e&&(t.preventDefault(),this.showContextMenu(t,e))}),document.addEventListener("click",t=>{this.refs.torrentContextMenu.contains(t.target)||this.hideContextMenu()}),this.refs.torrentContextMenu.addEventListener("click",t=>{const e=t.target.closest("[data-action]")?.dataset.action;e&&(this.handleContextAction(e),this.hideContextMenu())})}showContextMenu(t,e){this.state.selectedTorrentContextMenu={hash:e.dataset.hash,name:e.dataset.name,category:e.dataset.category||""},this.refs.torrentContextMenu.querySelector(".torrent-name").textContent=this.state.selectedTorrentContextMenu.name;const{pageX:s,pageY:r}=t,{clientWidth:n,clientHeight:a}=document.documentElement,o=this.refs.torrentContextMenu;o.style.left=`${Math.min(s,n-200)}px`,o.style.top=`${Math.min(r,a-150)}px`,o.classList.remove("hidden")}hideContextMenu(){this.refs.torrentContextMenu.classList.add("hidden"),this.state.selectedTorrentContextMenu=null}async handleContextAction(t){const e=this.state.selectedTorrentContextMenu;if(!e)return;const s={"copy-magnet":async()=>{try{await navigator.clipboard.writeText(`magnet:?xt=urn:btih:${e.hash}`),window.decypharrUtils.createToast("Magnet link copied to clipboard")}catch(t){window.decypharrUtils.createToast("Failed to copy magnet link","error")}},"copy-name":async()=>{try{await navigator.clipboard.writeText(e.name),window.decypharrUtils.createToast("Torrent name copied to clipboard")}catch(t){window.decypharrUtils.createToast("Failed to copy torrent name","error")}},delete:async()=>{await this.deleteTorrent(e.hash,e.category,!1)}};s[t]&&await s[t]()}async loadTorrents(){try{this.refs.refreshBtn.disabled=!0,this.refs.paginationInfo.textContent="Loading torrents...";const t=await window.decypharrUtils.fetcher("/api/torrents");if(!t.ok)throw new Error("Failed to fetch torrents");const e=await t.json();this.state.torrents=e,this.state.categories=new Set(e.map(t=>t.category).filter(Boolean)),this.updateUI()}catch(t){console.error("Error loading torrents:",t),window.decypharrUtils.createToast(`Error loading torrents: ${t.message}`,"error")}finally{this.refs.refreshBtn.disabled=!1}}updateUI(){this.filterTorrents(),this.updateCategoryFilter(),this.renderTorrents(),this.updatePagination(),this.updateSelectionUI(),this.toggleEmptyState()}filterTorrents(){let t=[...this.state.torrents];this.state.selectedCategory&&(t=t.filter(t=>t.category===this.state.selectedCategory)),this.state.selectedState&&(t=t.filter(t=>t.state?.toLowerCase()===this.state.selectedState.toLowerCase())),t=this.sortTorrents(t),this.state.filteredTorrents=t}sortTorrents(t){const[e,s]=this.state.sortBy.includes("_asc")||this.state.sortBy.includes("_desc")?[this.state.sortBy.split("_").slice(0,-1).join("_"),this.state.sortBy.endsWith("_asc")?"asc":"desc"]:[this.state.sortBy,"desc"];return t.sort((t,r)=>{let n,a;switch(e){case"name":n=t.name?.toLowerCase()||"",a=r.name?.toLowerCase()||"";break;case"size":n=t.size||0,a=r.size||0;break;case"progress":n=t.progress||0,a=r.progress||0;break;case"added_on":n=t.added_on||0,a=r.added_on||0;break;default:n=t[e]||0,a=r[e]||0}return"string"==typeof n?"asc"===s?n.localeCompare(a):a.localeCompare(n):"asc"===s?n-a:a-n})}renderTorrents(){const t=(this.state.currentPage-1)*this.state.itemsPerPage,e=Math.min(t+this.state.itemsPerPage,this.state.filteredTorrents.length),s=this.state.filteredTorrents.slice(t,e);this.refs.torrentsList.innerHTML=s.map(t=>this.torrentRowTemplate(t)).join("")}torrentRowTemplate(t){const e=(100*t.progress).toFixed(1),s=this.state.selectedTorrents.has(t.hash);new Date(t.added_on).toLocaleString();return`\n <tr data-hash="${t.hash}" \n data-name="${this.escapeHtml(t.name)}" \n data-category="${t.category||""}"\n class="hover:bg-base-200 transition-colors">\n <td>\n <label class="cursor-pointer">\n <input type="checkbox" \n class="checkbox checkbox-sm torrent-select" \n data-hash="${t.hash}" \n ${s?"checked":""}>\n </label>\n </td>\n <td class="max-w-xs">\n <div class="truncate font-medium" title="${this.escapeHtml(t.name)}">\n ${this.escapeHtml(t.name)}\n </div>\n </td>\n <td class="text-nowrap font-mono text-sm">\n ${window.decypharrUtils.formatBytes(t.size)}\n </td>\n <td class="min-w-36">\n <div class="flex items-center gap-3">\n <progress class="progress progress-primary w-20 h-2" \n value="${e}" \n max="100"></progress>\n <span class="text-sm font-medium min-w-12">${e}%</span>\n </div>\n </td>\n <td class="text-nowrap font-mono text-sm">\n ${window.decypharrUtils.formatSpeed(t.dlspeed)}\n </td>\n <td>\n ${t.category?`<div class="badge badge-secondary badge-sm">${this.escapeHtml(t.category)}</div>`:'<span class="text-base-content/50">None</span>'}\n </td>\n <td>\n ${t.debrid?`<div class="badge badge-accent badge-sm">${this.escapeHtml(t.debrid)}</div>`:'<span class="text-base-content/50">None</span>'}\n </td>\n <td class="text-nowrap font-mono text-sm">\n ${t.num_seeds||0}\n </td>\n <td>\n <div class="badge ${this.getStateColor(t.state)} badge-sm">\n ${this.escapeHtml(t.state)}\n </div>\n </td>\n <td>\n <div class="flex gap-1">\n <button class="btn btn-error btn-outline btn-xs tooltip" \n onclick="dashboard.deleteTorrent('${t.hash}', '${t.category||""}', false);"\n data-tip="Delete from local">\n <i class="bi bi-trash"></i>\n </button>\n ${t.debrid&&t.id?`\n <button class="btn btn-error btn-outline btn-xs tooltip" \n onclick="dashboard.deleteTorrent('${t.hash}', '${t.category||""}', true);"\n data-tip="Remove from ${t.debrid}">\n <i class="bi bi-cloud-slash"></i>\n </button>\n `:""}\n </div>\n </td>\n </tr>\n `}getStateColor(t){return{downloading:"badge-primary",pausedup:"badge-success",error:"badge-error",completed:"badge-success"}[t?.toLowerCase()]||"badge-ghost"}updateCategoryFilter(){const t=Array.from(this.state.categories).sort(),e=['<option value="">All Categories</option>'].concat(t.map(t=>`<option value="${this.escapeHtml(t)}" ${t===this.state.selectedCategory?"selected":""}>\n ${this.escapeHtml(t)}\n </option>`));this.refs.categoryFilter.innerHTML=e.join("")}updatePagination(){const t=Math.ceil(this.state.filteredTorrents.length/this.state.itemsPerPage),e=(this.state.currentPage-1)*this.state.itemsPerPage,s=Math.min(e+this.state.itemsPerPage,this.state.filteredTorrents.length);if(this.refs.paginationInfo.textContent=`Showing ${this.state.filteredTorrents.length>0?e+1:0}-${s} of ${this.state.filteredTorrents.length} torrents`,this.refs.paginationControls.innerHTML="",t<=1)return;const r=this.createPaginationButton("",this.state.currentPage-1,1===this.state.currentPage);this.refs.paginationControls.appendChild(r);let n=Math.max(1,this.state.currentPage-Math.floor(2.5)),a=Math.min(t,n+5-1);a-n+1<5&&(n=Math.max(1,a-5+1));for(let t=n;t<=a;t++){const e=this.createPaginationButton(t,t,!1,t===this.state.currentPage);this.refs.paginationControls.appendChild(e)}const o=this.createPaginationButton("",this.state.currentPage+1,this.state.currentPage===t);this.refs.paginationControls.appendChild(o)}createPaginationButton(t,e,s=!1,r=!1){const n=document.createElement("button");return n.className=`join-item btn btn-sm ${r?"btn-active":""} ${s?"btn-disabled":""}`,n.textContent=t,n.disabled=s,s||n.addEventListener("click",()=>{this.state.currentPage=e,this.updateUI()}),n}updateSelectionUI(){const t=new Set(this.state.filteredTorrents.map(t=>t.hash));this.state.selectedTorrents.forEach(e=>{t.has(e)||this.state.selectedTorrents.delete(e)}),this.refs.batchDeleteBtn.classList.toggle("hidden",0===this.state.selectedTorrents.size),this.refs.batchDeleteDebridBtn.classList.toggle("hidden",0===this.state.selectedTorrents.size);const e=this.state.filteredTorrents.slice((this.state.currentPage-1)*this.state.itemsPerPage,this.state.currentPage*this.state.itemsPerPage);this.refs.selectAll.checked=e.length>0&&e.every(t=>this.state.selectedTorrents.has(t.hash)),this.refs.selectAll.indeterminate=e.some(t=>this.state.selectedTorrents.has(t.hash))&&!e.every(t=>this.state.selectedTorrents.has(t.hash))}toggleEmptyState(){const t=0===this.state.torrents.length;this.refs.emptyState.classList.toggle("hidden",!t),document.querySelector(".card:has(#torrentsList)").classList.toggle("hidden",t)}setFilter(t,e){"category"===t?this.state.selectedCategory=e:"state"===t&&(this.state.selectedState=e),this.state.currentPage=1,this.updateUI()}setSort(t){this.state.sortBy=t,this.state.currentPage=1,this.updateUI()}toggleSelectAll(t){this.state.filteredTorrents.slice((this.state.currentPage-1)*this.state.itemsPerPage,this.state.currentPage*this.state.itemsPerPage).forEach(e=>{t?this.state.selectedTorrents.add(e.hash):this.state.selectedTorrents.delete(e.hash)}),this.updateUI()}toggleTorrentSelection(t,e){e?this.state.selectedTorrents.add(t):this.state.selectedTorrents.delete(t),this.updateSelectionUI()}async deleteTorrent(t,e,s=!1){if(confirm(`Are you sure you want to delete this torrent${s?" from "+e:""}?`))try{const r=`/api/torrents/${encodeURIComponent(e)}/${t}?removeFromDebrid=${s}`,n=await window.decypharrUtils.fetcher(r,{method:"DELETE"});if(!n.ok)throw new Error(await n.text());window.decypharrUtils.createToast("Torrent deleted successfully"),await this.loadTorrents()}catch(t){console.error("Error deleting torrent:",t),window.decypharrUtils.createToast(`Failed to delete torrent: ${t.message}`,"error")}}async deleteSelectedTorrents(t=!1){const e=this.state.selectedTorrents.size;if(0!==e){if(confirm(`Are you sure you want to delete ${e} torrent${e>1?"s":""}${t?" from debrid":""}?`))try{const s=Array.from(this.state.selectedTorrents).join(","),r=await window.decypharrUtils.fetcher(`/api/torrents/?hashes=${encodeURIComponent(s)}&removeFromDebrid=${t}`,{method:"DELETE"});if(!r.ok)throw new Error(await r.text());window.decypharrUtils.createToast(`${e} torrent${e>1?"s":""} deleted successfully`),this.state.selectedTorrents.clear(),await this.loadTorrents()}catch(t){console.error("Error deleting torrents:",t),window.decypharrUtils.createToast(`Failed to delete some torrents: ${t.message}`,"error")}}else window.decypharrUtils.createToast("No torrents selected for deletion","warning")}startAutoRefresh(){this.refreshInterval=setInterval(()=>{this.loadTorrents()},5e3),window.addEventListener("beforeunload",()=>{this.refreshInterval&&clearInterval(this.refreshInterval)})}escapeHtml(t){const e={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"};return t?t.replace(/[&<>"']/g,t=>e[t]):""}}