1 line
25 KiB
JavaScript
1 line
25 KiB
JavaScript
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={"&":"&","<":"<",">":">",'"':""","'":"'"};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)}} |