// Dashboard functionality for torrent and NZB management class Dashboard { constructor() { this.state = { mode: 'torrents', // 'torrents' or 'nzbs' torrents: [], nzbs: [], selectedItems: new Set(), categories: new Set(), filteredItems: [], selectedCategory: '', selectedState: '', sortBy: 'added_on', itemsPerPage: 20, currentPage: 1, selectedItemContextMenu: null }; this.refs = { // Mode switching torrentsMode: document.getElementById('torrentsMode'), nzbsMode: document.getElementById('nzbsMode'), // Table elements dataList: document.getElementById('dataList'), torrentsHeaders: document.getElementById('torrentsHeaders'), nzbsHeaders: document.getElementById('nzbsHeaders'), // Controls 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'), // Context menus torrentContextMenu: document.getElementById('torrentContextMenu'), nzbContextMenu: document.getElementById('nzbContextMenu'), // Pagination and empty state 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() { // Mode switching this.refs.torrentsMode.addEventListener('click', () => this.switchMode('torrents')); this.refs.nzbsMode.addEventListener('click', () => this.switchMode('nzbs')); // Refresh button this.refs.refreshBtn.addEventListener('click', () => this.loadData()); // Batch delete this.refs.batchDeleteBtn.addEventListener('click', () => this.deleteSelectedItems()); this.refs.batchDeleteDebridBtn.addEventListener('click', () => this.deleteSelectedItems(true)); // Select all checkboxes this.refs.selectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked)); this.refs.selectAllNzb.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked)); // Filters this.refs.categoryFilter.addEventListener('change', (e) => this.setFilter('category', e.target.value)); this.refs.stateFilter.addEventListener('change', (e) => this.setFilter('state', e.target.value)); this.refs.sortSelector.addEventListener('change', (e) => this.setSort(e.target.value)); // Context menu this.bindContextMenu(); // Item selection this.refs.dataList.addEventListener('change', (e) => { if (e.target.classList.contains('item-select')) { this.toggleItemSelection(e.target.dataset.id, e.target.checked); } }); } switchMode(mode) { if (this.state.mode === mode) return; this.state.mode = mode; this.state.selectedItems.clear(); // Update URL parameter this.updateURL(mode); // Update button states if (mode === 'torrents') { 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'); // Show torrent headers, hide NZB headers this.refs.torrentsHeaders.classList.remove('hidden'); this.refs.nzbsHeaders.classList.add('hidden'); // Update empty state this.refs.emptyStateTitle.textContent = 'No Torrents Found'; this.refs.emptyStateMessage.textContent = "You haven't added any torrents yet. Start by adding your first download!"; // Show debrid batch delete button this.refs.batchDeleteDebridBtn.classList.remove('hidden'); } else { 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'); // Show NZB headers, hide torrent headers this.refs.nzbsHeaders.classList.remove('hidden'); this.refs.torrentsHeaders.classList.add('hidden'); // Update empty state 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!"; // Hide debrid batch delete button (not relevant for NZBs) this.refs.batchDeleteDebridBtn.classList.add('hidden'); } // Reset filters and reload data this.state.selectedCategory = ''; this.state.selectedState = ''; this.state.currentPage = 1; this.refs.categoryFilter.value = ''; this.refs.stateFilter.value = ''; this.loadData(); this.updateBatchActions(); } updateBatchActions() { const hasSelection = this.state.selectedItems.size > 0; // Show/hide batch delete button if (this.refs.batchDeleteBtn) { this.refs.batchDeleteBtn.classList.toggle('hidden', !hasSelection); } // Show/hide debrid batch delete button (only for torrents) if (this.refs.batchDeleteDebridBtn) { const showDebridButton = hasSelection && this.state.mode === 'torrents'; this.refs.batchDeleteDebridBtn.classList.toggle('hidden', !showDebridButton); } // Update button text with count if (hasSelection) { const count = this.state.selectedItems.size; const itemType = this.state.mode === 'torrents' ? 'Torrent' : 'NZB'; const itemTypePlural = this.state.mode === 'torrents' ? 'Torrents' : 'NZBs'; if (this.refs.batchDeleteBtn) { const deleteText = count === 1 ? `Delete ${itemType}` : `Delete ${count} ${itemTypePlural}`; const deleteSpan = this.refs.batchDeleteBtn.querySelector('span'); if (deleteSpan) { deleteSpan.textContent = deleteText; } } if (this.refs.batchDeleteDebridBtn && this.state.mode === 'torrents') { const debridText = count === 1 ? 'Remove From Debrid' : `Remove ${count} From Debrid`; const debridSpan = this.refs.batchDeleteDebridBtn.querySelector('span'); if (debridSpan) { debridSpan.textContent = debridText; } } } else { // Reset button text when no selection if (this.refs.batchDeleteBtn) { const deleteSpan = this.refs.batchDeleteBtn.querySelector('span'); if (deleteSpan) { deleteSpan.textContent = 'Delete Selected'; } } if (this.refs.batchDeleteDebridBtn) { const debridSpan = this.refs.batchDeleteDebridBtn.querySelector('span'); if (debridSpan) { debridSpan.textContent = 'Remove From Debrid'; } } } } loadData() { if (this.state.mode === 'torrents') { this.loadTorrents(); } else { this.loadNZBs(); } } async loadNZBs() { try { const response = await window.decypharrUtils.fetcher('/api/nzbs'); if (!response.ok) { throw new Error('Failed to fetch NZBs'); } const data = await response.json(); this.state.nzbs = data.nzbs || []; this.updateCategories(); this.applyFilters(); this.renderData(); } catch (error) { console.error('Error loading NZBs:', error); window.decypharrUtils.createToast('Error loading NZBs', 'error'); } } updateCategories() { const items = this.state.mode === 'torrents' ? this.state.torrents : this.state.nzbs; this.state.categories = new Set(items.map(item => item.category).filter(Boolean)); } applyFilters() { if (this.state.mode === 'torrents') { this.filterTorrents(); } else { this.filterNZBs(); } } filterNZBs() { let filtered = [...this.state.nzbs]; if (this.state.selectedCategory) { filtered = filtered.filter(n => n.category === this.state.selectedCategory); } if (this.state.selectedState) { filtered = filtered.filter(n => n.status === this.state.selectedState); } // Apply sorting filtered.sort((a, b) => { switch (this.state.sortBy) { case 'added_on': return new Date(b.added_on) - new Date(a.added_on); case 'added_on_asc': return new Date(a.added_on) - new Date(b.added_on); case 'name_asc': return a.name.localeCompare(b.name); case 'name_desc': return b.name.localeCompare(a.name); case 'size_desc': return (b.total_size || 0) - (a.total_size || 0); case 'size_asc': return (a.total_size || 0) - (b.total_size || 0); case 'progress_desc': return (b.progress || 0) - (a.progress || 0); case 'progress_asc': return (a.progress || 0) - (b.progress || 0); default: return 0; } }); this.state.filteredItems = filtered; } renderData() { if (this.state.mode === 'torrents') { this.renderTorrents(); } else { this.renderNZBs(); } } renderNZBs() { const startIndex = (this.state.currentPage - 1) * this.state.itemsPerPage; const endIndex = startIndex + this.state.itemsPerPage; const pageItems = this.state.filteredItems.slice(startIndex, endIndex); const tbody = this.refs.dataList; tbody.innerHTML = ''; if (pageItems.length === 0) { this.refs.emptyState.classList.remove('hidden'); } else { this.refs.emptyState.classList.add('hidden'); pageItems.forEach(nzb => { const row = document.createElement('tr'); row.className = 'hover cursor-pointer'; row.setAttribute('data-id', nzb.id); row.setAttribute('data-name', nzb.name); row.setAttribute('data-category', nzb.category || ''); const progressPercent = Math.round(nzb.progress || 0); const sizeFormatted = this.formatBytes(nzb.total_size || 0); const etaFormatted = this.formatETA(nzb.eta || 0); const ageFormatted = this.formatAge(nzb.date_posted); const statusBadge = this.getStatusBadge(nzb.status); row.innerHTML = `
${nzb.name}
${sizeFormatted}
${progressPercent}%
${etaFormatted} ${nzb.category || 'N/A'} ${statusBadge} ${ageFormatted}
`; tbody.appendChild(row); }); } this.updatePagination(); this.updateSelectionUI(); } getStatusBadge(status) { const statusMap = { 'downloading': 'Downloading', 'completed': 'Completed', 'paused': 'Paused', 'failed': 'Failed', 'queued': 'Queued', 'processing': 'Processing', 'verifying': 'Verifying', 'repairing': 'Repairing', 'extracting': 'Extracting' }; return statusMap[status] || 'Unknown'; } formatETA(seconds) { if (!seconds || seconds <= 0) return 'N/A'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours > 0) { return `${hours}h ${minutes}m`; } else { return `${minutes}m`; } } formatAge(datePosted) { if (!datePosted) return 'N/A'; const now = new Date(); const posted = new Date(datePosted); const diffMs = now - posted; const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) { return 'Today'; } else if (diffDays === 1) { return '1 day'; } else { return `${diffDays} days`; } } formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } bindContextMenu() { // Show context menu this.refs.dataList.addEventListener('contextmenu', (e) => { const row = e.target.closest('tr[data-id]'); if (!row) return; e.preventDefault(); this.showContextMenu(e, row); }); // Hide context menu document.addEventListener('click', (e) => { const torrentMenu = this.refs.torrentContextMenu; const nzbMenu = this.refs.nzbContextMenu; if (!torrentMenu.contains(e.target) && !nzbMenu.contains(e.target)) { this.hideContextMenu(); } }); // Context menu actions for torrents this.refs.torrentContextMenu.addEventListener('click', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (action) { this.handleContextAction(action); this.hideContextMenu(); } }); // Context menu actions for NZBs this.refs.nzbContextMenu.addEventListener('click', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (action) { this.handleContextAction(action); this.hideContextMenu(); } }); } showContextMenu(event, row) { const { pageX, pageY } = event; const { clientWidth, clientHeight } = document.documentElement; if (this.state.mode === 'torrents') { this.state.selectedItemContextMenu = { id: row.dataset.hash, name: row.dataset.name, category: row.dataset.category || '', type: 'torrent' }; const menu = this.refs.torrentContextMenu; menu.querySelector('.torrent-name').textContent = this.state.selectedItemContextMenu.name; // Position the menu menu.style.left = `${Math.min(pageX, clientWidth - 200)}px`; menu.style.top = `${Math.min(pageY, clientHeight - 150)}px`; menu.classList.remove('hidden'); } else { this.state.selectedItemContextMenu = { id: row.dataset.id, name: row.dataset.name, category: row.dataset.category || '', type: 'nzb' }; const menu = this.refs.nzbContextMenu; menu.querySelector('.nzb-name').textContent = this.state.selectedItemContextMenu.name; // Position the menu menu.style.left = `${Math.min(pageX, clientWidth - 200)}px`; menu.style.top = `${Math.min(pageY, clientHeight - 150)}px`; menu.classList.remove('hidden'); } } hideContextMenu() { this.refs.torrentContextMenu.classList.add('hidden'); this.refs.nzbContextMenu.classList.add('hidden'); this.state.selectedItemContextMenu = null; } async handleContextAction(action) { const item = this.state.selectedItemContextMenu; if (!item) return; if (item.type === 'torrent') { await this.handleTorrentAction(action, item); } else { await this.handleNZBAction(action, item); } } async handleTorrentAction(action, torrent) { const actions = { 'copy-magnet': async () => { try { await navigator.clipboard.writeText(`magnet:?xt=urn:btih:${torrent.hash}`); window.decypharrUtils.createToast('Magnet link copied to clipboard'); } catch (error) { window.decypharrUtils.createToast('Failed to copy magnet link', 'error'); } }, 'copy-name': async () => { try { await navigator.clipboard.writeText(torrent.name); window.decypharrUtils.createToast('Torrent name copied to clipboard'); } catch (error) { window.decypharrUtils.createToast('Failed to copy torrent name', 'error'); } }, 'delete': async () => { await this.deleteTorrent(torrent.hash, torrent.category, false); } }; if (actions[action]) { await actions[action](); } } async handleNZBAction(action, nzb) { const actions = { 'pause': async () => { try { const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzb.id}/pause`, { method: 'POST' }); if (response.ok) { window.decypharrUtils.createToast('NZB paused successfully'); this.loadData(); } else { throw new Error('Failed to pause NZB'); } } catch (error) { window.decypharrUtils.createToast('Failed to pause NZB', 'error'); } }, 'resume': async () => { try { const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzb.id}/resume`, { method: 'POST' }); if (response.ok) { window.decypharrUtils.createToast('NZB resumed successfully'); this.loadData(); } else { throw new Error('Failed to resume NZB'); } } catch (error) { window.decypharrUtils.createToast('Failed to resume NZB', 'error'); } }, 'retry': async () => { try { const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzb.id}/retry`, { method: 'POST' }); if (response.ok) { window.decypharrUtils.createToast('NZB retry started successfully'); this.loadData(); } else { throw new Error('Failed to retry NZB'); } } catch (error) { window.decypharrUtils.createToast('Failed to retry NZB', 'error'); } }, 'copy-name': async () => { try { await navigator.clipboard.writeText(nzb.name); window.decypharrUtils.createToast('NZB name copied to clipboard'); } catch (error) { window.decypharrUtils.createToast('Failed to copy NZB name', 'error'); } }, 'delete': async () => { await this.deleteNZB(nzb.id); } }; if (actions[action]) { await actions[action](); } } async deleteNZB(nzbId) { try { const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzbId}`, { method: 'DELETE' }); if (response.ok) { window.decypharrUtils.createToast('NZB deleted successfully'); this.loadData(); } else { throw new Error('Failed to delete NZB'); } } catch (error) { window.decypharrUtils.createToast('Failed to delete NZB', 'error'); } } async loadTorrents() { try { // Show loading state this.refs.refreshBtn.disabled = true; this.refs.paginationInfo.textContent = 'Loading torrents...'; const response = await window.decypharrUtils.fetcher('/api/torrents'); if (!response.ok) throw new Error('Failed to fetch torrents'); const torrents = await response.json(); this.state.torrents = torrents; this.state.categories = new Set(torrents.map(t => t.category).filter(Boolean)); this.updateUI(); } catch (error) { console.error('Error loading torrents:', error); window.decypharrUtils.createToast(`Error loading torrents: ${error.message}`, 'error'); } finally { this.refs.refreshBtn.disabled = false; } } updateUI() { // Apply filters based on current mode this.applyFilters(); // Update category dropdown this.updateCategoryFilter(); // Render data table this.renderData(); // Update pagination this.updatePagination(); // Update selection state this.updateSelectionUI(); // Show/hide empty state this.toggleEmptyState(); } filterTorrents() { let filtered = [...this.state.torrents]; if (this.state.selectedCategory) { filtered = filtered.filter(t => t.category === this.state.selectedCategory); } if (this.state.selectedState) { filtered = filtered.filter(t => t.state?.toLowerCase() === this.state.selectedState.toLowerCase()); } // Sort torrents filtered = this.sortTorrents(filtered); this.state.filteredItems = filtered; } sortTorrents(torrents) { const [field, direction] = 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 torrents.sort((a, b) => { let valueA, valueB; switch (field) { case 'name': valueA = a.name?.toLowerCase() || ''; valueB = b.name?.toLowerCase() || ''; break; case 'size': valueA = a.size || 0; valueB = b.size || 0; break; case 'progress': valueA = a.progress || 0; valueB = b.progress || 0; break; case 'added_on': valueA = a.added_on || 0; valueB = b.added_on || 0; break; default: valueA = a[field] || 0; valueB = b[field] || 0; } if (typeof valueA === 'string') { return direction === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); } else { return direction === 'asc' ? valueA - valueB : valueB - valueA; } }); } renderTorrents() { const startIndex = (this.state.currentPage - 1) * this.state.itemsPerPage; const endIndex = Math.min(startIndex + this.state.itemsPerPage, this.state.filteredItems.length); const pageItems = this.state.filteredItems.slice(startIndex, endIndex); this.refs.dataList.innerHTML = pageItems.map(torrent => this.torrentRowTemplate(torrent)).join(''); } torrentRowTemplate(torrent) { const progressPercent = (torrent.progress * 100).toFixed(1); const isSelected = this.state.selectedItems.has(torrent.hash); let addedOn = new Date(torrent.added_on).toLocaleString(); return `
${this.escapeHtml(torrent.name)}
${window.decypharrUtils.formatBytes(torrent.size)}
${progressPercent}%
${window.decypharrUtils.formatSpeed(torrent.dlspeed)} ${torrent.category ? `
${this.escapeHtml(torrent.category)}
` : 'None' } ${torrent.debrid ? `
${this.escapeHtml(torrent.debrid)}
` : 'None' } ${torrent.num_seeds || 0}
${this.escapeHtml(torrent.state)}
${torrent.debrid && torrent.id ? ` ` : ''}
`; } getStateColor(state) { const stateColors = { 'downloading': 'badge-primary', 'pausedup': 'badge-success', 'error': 'badge-error', 'completed': 'badge-success' }; return stateColors[state?.toLowerCase()] || 'badge-ghost'; } updateCategoryFilter() { const currentCategories = Array.from(this.state.categories).sort(); const categoryOptions = [''] .concat(currentCategories.map(cat => `` )); this.refs.categoryFilter.innerHTML = categoryOptions.join(''); } updatePagination() { const totalPages = Math.ceil(this.state.filteredItems.length / this.state.itemsPerPage); const startIndex = (this.state.currentPage - 1) * this.state.itemsPerPage; const endIndex = Math.min(startIndex + this.state.itemsPerPage, this.state.filteredItems.length); // Update pagination info this.refs.paginationInfo.textContent = `Showing ${this.state.filteredItems.length > 0 ? startIndex + 1 : 0}-${endIndex} of ${this.state.filteredItems.length} torrents`; // Clear pagination controls this.refs.paginationControls.innerHTML = ''; if (totalPages <= 1) return; // Previous button const prevBtn = this.createPaginationButton('❮', this.state.currentPage - 1, this.state.currentPage === 1); this.refs.paginationControls.appendChild(prevBtn); // Page numbers const maxPageButtons = 5; let startPage = Math.max(1, this.state.currentPage - Math.floor(maxPageButtons / 2)); let endPage = Math.min(totalPages, startPage + maxPageButtons - 1); if (endPage - startPage + 1 < maxPageButtons) { startPage = Math.max(1, endPage - maxPageButtons + 1); } for (let i = startPage; i <= endPage; i++) { const pageBtn = this.createPaginationButton(i, i, false, i === this.state.currentPage); this.refs.paginationControls.appendChild(pageBtn); } // Next button const nextBtn = this.createPaginationButton('❯', this.state.currentPage + 1, this.state.currentPage === totalPages); this.refs.paginationControls.appendChild(nextBtn); } createPaginationButton(text, page, disabled = false, active = false) { const button = document.createElement('button'); button.className = `join-item btn btn-sm ${active ? 'btn-active' : ''} ${disabled ? 'btn-disabled' : ''}`; button.textContent = text; button.disabled = disabled; if (!disabled) { button.addEventListener('click', () => { this.state.currentPage = page; this.updateUI(); }); } return button; } updateSelectionUI() { // Clean up selected torrents that no longer exist const currentHashes = new Set(this.state.filteredItems.map(t => t.hash)); this.state.selectedItems.forEach(hash => { if (!currentHashes.has(hash)) { this.state.selectedItems.delete(hash); } }); // Update batch delete button this.refs.batchDeleteBtn.classList.toggle('hidden', this.state.selectedItems.size === 0); this.refs.batchDeleteDebridBtn.classList.toggle('hidden', this.state.selectedItems.size === 0); // Update select all checkbox const visibleTorrents = this.state.filteredItems.slice( (this.state.currentPage - 1) * this.state.itemsPerPage, this.state.currentPage * this.state.itemsPerPage ); this.refs.selectAll.checked = visibleTorrents.length > 0 && visibleTorrents.every(torrent => this.state.selectedItems.has(torrent.hash)); this.refs.selectAll.indeterminate = visibleTorrents.some(torrent => this.state.selectedItems.has(torrent.hash)) && !visibleTorrents.every(torrent => this.state.selectedItems.has(torrent.hash)); } toggleEmptyState() { const items = this.state.mode === 'torrents' ? this.state.torrents : this.state.nzbs; const isEmpty = items.length === 0; if (this.refs.emptyState) { this.refs.emptyState.classList.toggle('hidden', !isEmpty); } // Find the main data table card and toggle its visibility const dataTableCard = document.querySelector('.card:has(#dataList)'); if (dataTableCard) { dataTableCard.classList.toggle('hidden', isEmpty); } } // Event handlers setFilter(type, value) { if (type === 'category') { this.state.selectedCategory = value; } else if (type === 'state') { this.state.selectedState = value; } this.state.currentPage = 1; this.updateUI(); } setSort(sortBy) { this.state.sortBy = sortBy; this.state.currentPage = 1; this.updateUI(); } toggleSelectAll(checked) { const visibleTorrents = this.state.filteredItems.slice( (this.state.currentPage - 1) * this.state.itemsPerPage, this.state.currentPage * this.state.itemsPerPage ); visibleTorrents.forEach(torrent => { if (checked) { this.state.selectedItems.add(torrent.hash); } else { this.state.selectedItems.delete(torrent.hash); } }); this.updateUI(); } toggleItemSelection(id, checked) { if (checked) { this.state.selectedItems.add(id); } else { this.state.selectedItems.delete(id); } this.updateSelectionUI(); this.updateBatchActions(); } async deleteTorrent(hash, category, removeFromDebrid = false) { if (!confirm(`Are you sure you want to delete this torrent${removeFromDebrid ? ' from ' + category : ''}?`)) { return; } try { const endpoint = `/api/torrents/${encodeURIComponent(category)}/${hash}?removeFromDebrid=${removeFromDebrid}`; const response = await window.decypharrUtils.fetcher(endpoint, { method: 'DELETE' }); if (!response.ok) throw new Error(await response.text()); window.decypharrUtils.createToast('Torrent deleted successfully'); await this.loadTorrents(); } catch (error) { console.error('Error deleting torrent:', error); window.decypharrUtils.createToast(`Failed to delete torrent: ${error.message}`, 'error'); } } async deleteSelectedItems(removeFromDebrid = false) { const count = this.state.selectedItems.size; if (count === 0) { const itemType = this.state.mode === 'torrents' ? 'torrents' : 'NZBs'; window.decypharrUtils.createToast(`No ${itemType} selected for deletion`, 'warning'); return; } const itemType = this.state.mode === 'torrents' ? 'torrent' : 'NZB'; const itemTypePlural = this.state.mode === 'torrents' ? 'torrents' : 'NZBs'; if (!confirm(`Are you sure you want to delete ${count} ${count > 1 ? itemTypePlural : itemType}${removeFromDebrid ? ' from debrid' : ''}?`)) { return; } try { if (this.state.mode === 'torrents') { const hashes = Array.from(this.state.selectedItems).join(','); const response = await window.decypharrUtils.fetcher( `/api/torrents/?hashes=${encodeURIComponent(hashes)}&removeFromDebrid=${removeFromDebrid}`, { method: 'DELETE' } ); if (!response.ok) throw new Error(await response.text()); } else { // Delete NZBs one by one const promises = Array.from(this.state.selectedItems).map(id => window.decypharrUtils.fetcher(`/api/nzbs/${id}`, { method: 'DELETE' }) ); const responses = await Promise.all(promises); for (const response of responses) { if (!response.ok) throw new Error(await response.text()); } } window.decypharrUtils.createToast(`${count} ${count > 1 ? itemTypePlural : itemType} deleted successfully`); this.state.selectedItems.clear(); await this.loadData(); } catch (error) { console.error(`Error deleting ${itemTypePlural}:`, error); window.decypharrUtils.createToast(`Failed to delete some ${itemTypePlural}: ${error.message}`, 'error'); } } startAutoRefresh() { this.refreshInterval = setInterval(() => { this.loadData(); }, 5000); // Clean up on page unload window.addEventListener('beforeunload', () => { if (this.refreshInterval) { clearInterval(this.refreshInterval); } }); } escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text ? text.replace(/[&<>"']/g, (m) => map[m]) : ''; } loadModeFromURL() { const urlParams = new URLSearchParams(window.location.search); const mode = urlParams.get('mode'); if (mode === 'nzbs' || mode === 'torrents') { this.state.mode = mode; } else { this.state.mode = 'torrents'; // Default mode } // Set the initial UI state without triggering reload this.setModeUI(this.state.mode); } setModeUI(mode) { if (mode === 'torrents') { 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'); } else { 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(mode) { const url = new URL(window.location); url.searchParams.set('mode', mode); window.history.replaceState({}, '', url); } }