Files
decypharr/pkg/web/assets/build/js/dashboard.js
2025-08-01 15:27:24 +01:00

1 line
25 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 Dashboard{constructor(){this.state={mode:"torrents",torrents:[],nzbs:[],selectedItems:new Set,categories:new Set,filteredItems:[],selectedCategory:"",selectedState:"",sortBy:"added_on",itemsPerPage:20,currentPage:1,selectedItemContextMenu:null},this.refs={torrentsMode:document.getElementById("torrentsMode"),nzbsMode:document.getElementById("nzbsMode"),dataList:document.getElementById("dataList"),torrentsHeaders:document.getElementById("torrentsHeaders"),nzbsHeaders:document.getElementById("nzbsHeaders"),categoryFilter:document.getElementById("categoryFilter"),stateFilter:document.getElementById("stateFilter"),sortSelector:document.getElementById("sortSelector"),selectAll:document.getElementById("selectAll"),selectAllNzb:document.getElementById("selectAllNzb"),batchDeleteBtn:document.getElementById("batchDeleteBtn"),batchDeleteDebridBtn:document.getElementById("batchDeleteDebridBtn"),refreshBtn:document.getElementById("refreshBtn"),torrentContextMenu:document.getElementById("torrentContextMenu"),nzbContextMenu:document.getElementById("nzbContextMenu"),paginationControls:document.getElementById("paginationControls"),paginationInfo:document.getElementById("paginationInfo"),emptyState:document.getElementById("emptyState"),emptyStateTitle:document.getElementById("emptyStateTitle"),emptyStateMessage:document.getElementById("emptyStateMessage")},this.init()}init(){this.bindEvents(),this.loadModeFromURL(),this.loadData(),this.startAutoRefresh()}bindEvents(){this.refs.torrentsMode.addEventListener("click",()=>this.switchMode("torrents")),this.refs.nzbsMode.addEventListener("click",()=>this.switchMode("nzbs")),this.refs.refreshBtn.addEventListener("click",()=>this.loadData()),this.refs.batchDeleteBtn.addEventListener("click",()=>this.deleteSelectedItems()),this.refs.batchDeleteDebridBtn.addEventListener("click",()=>this.deleteSelectedItems(!0)),this.refs.selectAll.addEventListener("change",t=>this.toggleSelectAll(t.target.checked)),this.refs.selectAllNzb.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.dataList.addEventListener("change",t=>{t.target.classList.contains("item-select")&&this.toggleItemSelection(t.target.dataset.id,t.target.checked)})}switchMode(t){this.state.mode!==t&&(this.state.mode=t,this.state.selectedItems.clear(),this.updateURL(t),"torrents"===t?(this.refs.torrentsMode.classList.remove("btn-outline"),this.refs.torrentsMode.classList.add("btn-primary"),this.refs.nzbsMode.classList.remove("btn-primary"),this.refs.nzbsMode.classList.add("btn-outline"),this.refs.torrentsHeaders.classList.remove("hidden"),this.refs.nzbsHeaders.classList.add("hidden"),this.refs.emptyStateTitle.textContent="No Torrents Found",this.refs.emptyStateMessage.textContent="You haven't added any torrents yet. Start by adding your first download!",this.refs.batchDeleteDebridBtn.classList.remove("hidden")):(this.refs.nzbsMode.classList.remove("btn-outline"),this.refs.nzbsMode.classList.add("btn-primary"),this.refs.torrentsMode.classList.remove("btn-primary"),this.refs.torrentsMode.classList.add("btn-outline"),this.refs.nzbsHeaders.classList.remove("hidden"),this.refs.torrentsHeaders.classList.add("hidden"),this.refs.emptyStateTitle.textContent="No NZBs Found",this.refs.emptyStateMessage.textContent="You haven't added any NZB downloads yet. Start by adding your first NZB!",this.refs.batchDeleteDebridBtn.classList.add("hidden")),this.state.selectedCategory="",this.state.selectedState="",this.state.currentPage=1,this.refs.categoryFilter.value="",this.refs.stateFilter.value="",this.loadData(),this.updateBatchActions())}updateBatchActions(){const t=this.state.selectedItems.size>0;if(this.refs.batchDeleteBtn&&this.refs.batchDeleteBtn.classList.toggle("hidden",!t),this.refs.batchDeleteDebridBtn){const e=t&&"torrents"===this.state.mode;this.refs.batchDeleteDebridBtn.classList.toggle("hidden",!e)}if(t){const t=this.state.selectedItems.size,e="torrents"===this.state.mode?"Torrent":"NZB",s="torrents"===this.state.mode?"Torrents":"NZBs";if(this.refs.batchDeleteBtn){const a=1===t?`Delete ${e}`:`Delete ${t} ${s}`,r=this.refs.batchDeleteBtn.querySelector("span");r&&(r.textContent=a)}if(this.refs.batchDeleteDebridBtn&&"torrents"===this.state.mode){const e=1===t?"Remove From Debrid":`Remove ${t} From Debrid`,s=this.refs.batchDeleteDebridBtn.querySelector("span");s&&(s.textContent=e)}}else{if(this.refs.batchDeleteBtn){const t=this.refs.batchDeleteBtn.querySelector("span");t&&(t.textContent="Delete Selected")}if(this.refs.batchDeleteDebridBtn){const t=this.refs.batchDeleteDebridBtn.querySelector("span");t&&(t.textContent="Remove From Debrid")}}}loadData(){"torrents"===this.state.mode?this.loadTorrents():this.loadNZBs()}async loadNZBs(){try{const t=await window.decypharrUtils.fetcher("/api/nzbs");if(!t.ok)throw new Error("Failed to fetch NZBs");const e=await t.json();this.state.nzbs=e.nzbs||[],this.updateCategories(),this.applyFilters(),this.renderData()}catch(t){console.error("Error loading NZBs:",t),window.decypharrUtils.createToast("Error loading NZBs","error")}}updateCategories(){const t="torrents"===this.state.mode?this.state.torrents:this.state.nzbs;this.state.categories=new Set(t.map(t=>t.category).filter(Boolean))}applyFilters(){"torrents"===this.state.mode?this.filterTorrents():this.filterNZBs()}filterNZBs(){let t=[...this.state.nzbs];this.state.selectedCategory&&(t=t.filter(t=>t.category===this.state.selectedCategory)),this.state.selectedState&&(t=t.filter(t=>t.status===this.state.selectedState)),t.sort((t,e)=>{switch(this.state.sortBy){case"added_on":return new Date(e.added_on)-new Date(t.added_on);case"added_on_asc":return new Date(t.added_on)-new Date(e.added_on);case"name_asc":return t.name.localeCompare(e.name);case"name_desc":return e.name.localeCompare(t.name);case"size_desc":return(e.total_size||0)-(t.total_size||0);case"size_asc":return(t.total_size||0)-(e.total_size||0);case"progress_desc":return(e.progress||0)-(t.progress||0);case"progress_asc":return(t.progress||0)-(e.progress||0);default:return 0}}),this.state.filteredItems=t}renderData(){"torrents"===this.state.mode?this.renderTorrents():this.renderNZBs()}renderNZBs(){const t=(this.state.currentPage-1)*this.state.itemsPerPage,e=t+this.state.itemsPerPage,s=this.state.filteredItems.slice(t,e),a=this.refs.dataList;a.innerHTML="",0===s.length?this.refs.emptyState.classList.remove("hidden"):(this.refs.emptyState.classList.add("hidden"),s.forEach(t=>{const e=document.createElement("tr");e.className="hover cursor-pointer",e.setAttribute("data-id",t.id),e.setAttribute("data-name",t.name),e.setAttribute("data-category",t.category||"");const s=Math.round(t.progress||0),r=this.formatBytes(t.total_size||0),n=this.formatETA(t.eta||0),i=this.formatAge(t.date_posted),o=this.getStatusBadge(t.status);e.innerHTML=`\n <td class="w-12">\n <label class="cursor-pointer">\n <input type="checkbox" class="checkbox checkbox-sm item-select" data-id="${t.id}">\n </label>\n </td>\n <td class="font-medium max-w-xs">\n <div class="truncate" title="${t.name}">${t.name}</div>\n </td>\n <td>${r}</td>\n <td>\n <div class="flex items-center gap-2">\n <div class="w-16 bg-base-300 rounded-full h-2">\n <div class="bg-primary h-2 rounded-full transition-all duration-300" style="width: ${s}%"></div>\n </div>\n <span class="text-sm font-medium">${s}%</span>\n </div>\n </td>\n <td>${n}</td>\n <td>\n <span class="badge badge-ghost badge-sm">${t.category||"N/A"}</span>\n </td>\n <td>${o}</td>\n <td>${i}</td>\n <td>\n <div class="flex gap-1">\n <button class="btn btn-ghost btn-xs" onclick="window.dashboard.deleteNZB('${t.id}');" title="Delete">\n <i class="bi bi-trash"></i>\n </button>\n </div>\n </td>\n `,a.appendChild(e)})),this.updatePagination(),this.updateSelectionUI()}getStatusBadge(t){return{downloading:'<span class="badge badge-info badge-sm">Downloading</span>',completed:'<span class="badge badge-success badge-sm">Completed</span>',paused:'<span class="badge badge-warning badge-sm">Paused</span>',failed:'<span class="badge badge-error badge-sm">Failed</span>',queued:'<span class="badge badge-ghost badge-sm">Queued</span>',processing:'<span class="badge badge-info badge-sm">Processing</span>',verifying:'<span class="badge badge-info badge-sm">Verifying</span>',repairing:'<span class="badge badge-warning badge-sm">Repairing</span>',extracting:'<span class="badge badge-info badge-sm">Extracting</span>'}[t]||'<span class="badge badge-ghost badge-sm">Unknown</span>'}formatETA(t){if(!t||t<=0)return"N/A";const e=Math.floor(t/3600),s=Math.floor(t%3600/60);return e>0?`${e}h ${s}m`:`${s}m`}formatAge(t){if(!t)return"N/A";const e=new Date-new Date(t),s=Math.floor(e/864e5);return 0===s?"Today":1===s?"1 day":`${s} days`}formatBytes(t){if(!t||0===t)return"0 B";const e=Math.floor(Math.log(t)/Math.log(1024));return parseFloat((t/Math.pow(1024,e)).toFixed(2))+" "+["B","KB","MB","GB","TB"][e]}bindContextMenu(){this.refs.dataList.addEventListener("contextmenu",t=>{const e=t.target.closest("tr[data-id]");e&&(t.preventDefault(),this.showContextMenu(t,e))}),document.addEventListener("click",t=>{const e=this.refs.torrentContextMenu,s=this.refs.nzbContextMenu;e.contains(t.target)||s.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())}),this.refs.nzbContextMenu.addEventListener("click",t=>{const e=t.target.closest("[data-action]")?.dataset.action;e&&(this.handleContextAction(e),this.hideContextMenu())})}showContextMenu(t,e){const{pageX:s,pageY:a}=t,{clientWidth:r,clientHeight:n}=document.documentElement;if("torrents"===this.state.mode){this.state.selectedItemContextMenu={id:e.dataset.hash,name:e.dataset.name,category:e.dataset.category||"",type:"torrent"};const t=this.refs.torrentContextMenu;t.querySelector(".torrent-name").textContent=this.state.selectedItemContextMenu.name,t.style.left=`${Math.min(s,r-200)}px`,t.style.top=`${Math.min(a,n-150)}px`,t.classList.remove("hidden")}else{this.state.selectedItemContextMenu={id:e.dataset.id,name:e.dataset.name,category:e.dataset.category||"",type:"nzb"};const t=this.refs.nzbContextMenu;t.querySelector(".nzb-name").textContent=this.state.selectedItemContextMenu.name,t.style.left=`${Math.min(s,r-200)}px`,t.style.top=`${Math.min(a,n-150)}px`,t.classList.remove("hidden")}}hideContextMenu(){this.refs.torrentContextMenu.classList.add("hidden"),this.refs.nzbContextMenu.classList.add("hidden"),this.state.selectedItemContextMenu=null}async handleContextAction(t){const e=this.state.selectedItemContextMenu;e&&("torrent"===e.type?await this.handleTorrentAction(t,e):await this.handleNZBAction(t,e))}async handleTorrentAction(t,e){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 handleNZBAction(t,e){const s={pause:async()=>{try{if(!(await window.decypharrUtils.fetcher(`/api/nzbs/${e.id}/pause`,{method:"POST"})).ok)throw new Error("Failed to pause NZB");window.decypharrUtils.createToast("NZB paused successfully"),this.loadData()}catch(t){window.decypharrUtils.createToast("Failed to pause NZB","error")}},resume:async()=>{try{if(!(await window.decypharrUtils.fetcher(`/api/nzbs/${e.id}/resume`,{method:"POST"})).ok)throw new Error("Failed to resume NZB");window.decypharrUtils.createToast("NZB resumed successfully"),this.loadData()}catch(t){window.decypharrUtils.createToast("Failed to resume NZB","error")}},retry:async()=>{try{if(!(await window.decypharrUtils.fetcher(`/api/nzbs/${e.id}/retry`,{method:"POST"})).ok)throw new Error("Failed to retry NZB");window.decypharrUtils.createToast("NZB retry started successfully"),this.loadData()}catch(t){window.decypharrUtils.createToast("Failed to retry NZB","error")}},"copy-name":async()=>{try{await navigator.clipboard.writeText(e.name),window.decypharrUtils.createToast("NZB name copied to clipboard")}catch(t){window.decypharrUtils.createToast("Failed to copy NZB name","error")}},delete:async()=>{await this.deleteNZB(e.id)}};s[t]&&await s[t]()}async deleteNZB(t){try{if(!(await window.decypharrUtils.fetcher(`/api/nzbs/${t}`,{method:"DELETE"})).ok)throw new Error("Failed to delete NZB");window.decypharrUtils.createToast("NZB deleted successfully"),this.loadData()}catch(t){window.decypharrUtils.createToast("Failed to delete NZB","error")}}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.applyFilters(),this.updateCategoryFilter(),this.renderData(),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.filteredItems=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,a)=>{let r,n;switch(e){case"name":r=t.name?.toLowerCase()||"",n=a.name?.toLowerCase()||"";break;case"size":r=t.size||0,n=a.size||0;break;case"progress":r=t.progress||0,n=a.progress||0;break;case"added_on":r=t.added_on||0,n=a.added_on||0;break;default:r=t[e]||0,n=a[e]||0}return"string"==typeof r?"asc"===s?r.localeCompare(n):n.localeCompare(r):"asc"===s?r-n:n-r})}renderTorrents(){const t=(this.state.currentPage-1)*this.state.itemsPerPage,e=Math.min(t+this.state.itemsPerPage,this.state.filteredItems.length),s=this.state.filteredItems.slice(t,e);this.refs.dataList.innerHTML=s.map(t=>this.torrentRowTemplate(t)).join("")}torrentRowTemplate(t){const e=(100*t.progress).toFixed(1),s=this.state.selectedItems.has(t.hash);new Date(t.added_on).toLocaleString();return`\n <tr data-id="${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 item-select" \n data-id="${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.filteredItems.length/this.state.itemsPerPage),e=(this.state.currentPage-1)*this.state.itemsPerPage,s=Math.min(e+this.state.itemsPerPage,this.state.filteredItems.length);if(this.refs.paginationInfo.textContent=`Showing ${this.state.filteredItems.length>0?e+1:0}-${s} of ${this.state.filteredItems.length} torrents`,this.refs.paginationControls.innerHTML="",t<=1)return;const a=this.createPaginationButton("",this.state.currentPage-1,1===this.state.currentPage);this.refs.paginationControls.appendChild(a);let r=Math.max(1,this.state.currentPage-Math.floor(2.5)),n=Math.min(t,r+5-1);n-r+1<5&&(r=Math.max(1,n-5+1));for(let t=r;t<=n;t++){const e=this.createPaginationButton(t,t,!1,t===this.state.currentPage);this.refs.paginationControls.appendChild(e)}const i=this.createPaginationButton("",this.state.currentPage+1,this.state.currentPage===t);this.refs.paginationControls.appendChild(i)}createPaginationButton(t,e,s=!1,a=!1){const r=document.createElement("button");return r.className=`join-item btn btn-sm ${a?"btn-active":""} ${s?"btn-disabled":""}`,r.textContent=t,r.disabled=s,s||r.addEventListener("click",()=>{this.state.currentPage=e,this.updateUI()}),r}updateSelectionUI(){const t=new Set(this.state.filteredItems.map(t=>t.hash));this.state.selectedItems.forEach(e=>{t.has(e)||this.state.selectedItems.delete(e)}),this.refs.batchDeleteBtn.classList.toggle("hidden",0===this.state.selectedItems.size),this.refs.batchDeleteDebridBtn.classList.toggle("hidden",0===this.state.selectedItems.size);const e=this.state.filteredItems.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.selectedItems.has(t.hash)),this.refs.selectAll.indeterminate=e.some(t=>this.state.selectedItems.has(t.hash))&&!e.every(t=>this.state.selectedItems.has(t.hash))}toggleEmptyState(){const t=0===("torrents"===this.state.mode?this.state.torrents:this.state.nzbs).length;this.refs.emptyState&&this.refs.emptyState.classList.toggle("hidden",!t);const e=document.querySelector(".card:has(#dataList)");e&&e.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.filteredItems.slice((this.state.currentPage-1)*this.state.itemsPerPage,this.state.currentPage*this.state.itemsPerPage).forEach(e=>{t?this.state.selectedItems.add(e.hash):this.state.selectedItems.delete(e.hash)}),this.updateUI()}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t),this.updateSelectionUI(),this.updateBatchActions()}async deleteTorrent(t,e,s=!1){if(confirm(`Are you sure you want to delete this torrent${s?" from "+e:""}?`))try{const a=`/api/torrents/${encodeURIComponent(e)}/${t}?removeFromDebrid=${s}`,r=await window.decypharrUtils.fetcher(a,{method:"DELETE"});if(!r.ok)throw new Error(await r.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 deleteSelectedItems(t=!1){const e=this.state.selectedItems.size;if(0===e){const t="torrents"===this.state.mode?"torrents":"NZBs";return void window.decypharrUtils.createToast(`No ${t} selected for deletion`,"warning")}const s="torrents"===this.state.mode?"torrent":"NZB",a="torrents"===this.state.mode?"torrents":"NZBs";if(confirm(`Are you sure you want to delete ${e} ${e>1?a:s}${t?" from debrid":""}?`))try{if("torrents"===this.state.mode){const e=Array.from(this.state.selectedItems).join(","),s=await window.decypharrUtils.fetcher(`/api/torrents/?hashes=${encodeURIComponent(e)}&removeFromDebrid=${t}`,{method:"DELETE"});if(!s.ok)throw new Error(await s.text())}else{const t=Array.from(this.state.selectedItems).map(t=>window.decypharrUtils.fetcher(`/api/nzbs/${t}`,{method:"DELETE"})),e=await Promise.all(t);for(const t of e)if(!t.ok)throw new Error(await t.text())}window.decypharrUtils.createToast(`${e} ${e>1?a:s} deleted successfully`),this.state.selectedItems.clear(),await this.loadData()}catch(t){console.error(`Error deleting ${a}:`,t),window.decypharrUtils.createToast(`Failed to delete some ${a}: ${t.message}`,"error")}}startAutoRefresh(){this.refreshInterval=setInterval(()=>{this.loadData()},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]):""}loadModeFromURL(){const t=new URLSearchParams(window.location.search).get("mode");this.state.mode="nzbs"===t||"torrents"===t?t:"torrents",this.setModeUI(this.state.mode)}setModeUI(t){"torrents"===t?(this.refs.torrentsMode.classList.remove("btn-outline"),this.refs.torrentsMode.classList.add("btn-primary"),this.refs.nzbsMode.classList.remove("btn-primary"),this.refs.nzbsMode.classList.add("btn-outline"),this.refs.torrentsHeaders.classList.remove("hidden"),this.refs.nzbsHeaders.classList.add("hidden"),this.refs.emptyStateTitle.textContent="No Torrents Found",this.refs.emptyStateMessage.textContent="You haven't added any torrents yet. Start by adding your first download!",this.refs.batchDeleteDebridBtn.classList.remove("hidden")):(this.refs.nzbsMode.classList.remove("btn-outline"),this.refs.nzbsMode.classList.add("btn-primary"),this.refs.torrentsMode.classList.remove("btn-primary"),this.refs.torrentsMode.classList.add("btn-outline"),this.refs.nzbsHeaders.classList.remove("hidden"),this.refs.torrentsHeaders.classList.add("hidden"),this.refs.emptyStateTitle.textContent="No NZBs Found",this.refs.emptyStateMessage.textContent="You haven't added any NZB downloads yet. Start by adding your first NZB!",this.refs.batchDeleteDebridBtn.classList.add("hidden"))}updateURL(t){const e=new URL(window.location);e.searchParams.set("mode",t),window.history.replaceState({},"",e)}}