// Dashboard functionality for torrent management 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() { // Refresh button this.refs.refreshBtn.addEventListener('click', () => this.loadTorrents()); // Batch delete this.refs.batchDeleteBtn.addEventListener('click', () => this.deleteSelectedTorrents()); this.refs.batchDeleteDebridBtn.addEventListener('click', () => this.deleteSelectedTorrents(true)); // Select all checkbox this.refs.selectAll.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(); // Torrent selection this.refs.torrentsList.addEventListener('change', (e) => { if (e.target.classList.contains('torrent-select')) { this.toggleTorrentSelection(e.target.dataset.hash, e.target.checked); } }); } bindContextMenu() { // Show context menu this.refs.torrentsList.addEventListener('contextmenu', (e) => { const row = e.target.closest('tr[data-hash]'); if (!row) return; e.preventDefault(); this.showContextMenu(e, row); }); // Hide context menu document.addEventListener('click', (e) => { if (!this.refs.torrentContextMenu.contains(e.target)) { this.hideContextMenu(); } }); // Context menu actions this.refs.torrentContextMenu.addEventListener('click', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (action) { this.handleContextAction(action); this.hideContextMenu(); } }); } showContextMenu(event, row) { this.state.selectedTorrentContextMenu = { hash: row.dataset.hash, name: row.dataset.name, category: row.dataset.category || '' }; this.refs.torrentContextMenu.querySelector('.torrent-name').textContent = this.state.selectedTorrentContextMenu.name; const { pageX, pageY } = event; const { clientWidth, clientHeight } = document.documentElement; const menu = this.refs.torrentContextMenu; // 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.state.selectedTorrentContextMenu = null; } async handleContextAction(action) { const torrent = this.state.selectedTorrentContextMenu; if (!torrent) return; 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 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() { // Filter torrents this.filterTorrents(); // Update category dropdown this.updateCategoryFilter(); // Render torrents table this.renderTorrents(); // 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.filteredTorrents = 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.filteredTorrents.length); const pageItems = this.state.filteredTorrents.slice(startIndex, endIndex); this.refs.torrentsList.innerHTML = pageItems.map(torrent => this.torrentRowTemplate(torrent)).join(''); } torrentRowTemplate(torrent) { const progressPercent = (torrent.progress * 100).toFixed(1); const isSelected = this.state.selectedTorrents.has(torrent.hash); let addedOn = new Date(torrent.added_on).toLocaleString(); return `