diff --git a/public/app.js b/public/app.js index 056b88c..1e66751 100644 --- a/public/app.js +++ b/public/app.js @@ -2,12 +2,34 @@ let currentUser = null; let downloads = []; let downloadClients = []; // List of download clients from server (for ordering/filtering) -let selectedDownloadClient = localStorage.getItem('sofarr-download-client') || 'all'; // Selected client filter +let selectedDownloadClients = []; // Array of selected client IDs for multi-select filter let isAdmin = false; let showAll = false; let csrfToken = null; // double-submit CSRF token, sent as X-CSRF-Token on mutating requests const SPLASH_MIN_MS = 1200; // minimum splash display time +// Migration from old single-select to new multi-select format +(function migrateDownloadClientFilter() { + const oldSelection = localStorage.getItem('sofarr-download-client'); + if (oldSelection && oldSelection !== 'all') { + try { + selectedDownloadClients = [oldSelection]; + localStorage.setItem('sofarr-download-clients', JSON.stringify(selectedDownloadClients)); + localStorage.removeItem('sofarr-download-client'); + } catch (e) { + console.error('[Migration] Failed to migrate download client filter:', e); + } + } else { + try { + const newSelection = localStorage.getItem('sofarr-download-clients'); + selectedDownloadClients = newSelection ? JSON.parse(newSelection) : []; + } catch (e) { + console.error('[Migration] Failed to load download client filter:', e); + selectedDownloadClients = []; + } + } +})(); + // History section state let historyDays = parseInt(localStorage.getItem('sofarr-history-days'), 10) || 7; let historyRefreshHandle = null; @@ -361,10 +383,10 @@ function renderDownloads() { const downloadsList = document.getElementById('downloads-list'); const noDownloads = document.getElementById('no-downloads'); - // Filter downloads by selected client + // Filter downloads by selected clients let filteredDownloads = downloads; - if (selectedDownloadClient !== 'all') { - filteredDownloads = downloads.filter(d => d.instanceId === selectedDownloadClient); + if (selectedDownloadClients.length > 0) { + filteredDownloads = downloads.filter(d => selectedDownloadClients.includes(d.instanceId)); } // Sort downloads by client order (matching the order in downloadClients) @@ -1051,43 +1073,141 @@ function initHistoryControls() { // ============================================================================= function initDownloadClientFilter() { - const filterSelect = document.getElementById('download-client-filter'); - if (filterSelect) { - // Set initial value from localStorage - filterSelect.value = selectedDownloadClient; - filterSelect.addEventListener('change', () => { - selectedDownloadClient = filterSelect.value; - localStorage.setItem('sofarr-download-client', selectedDownloadClient); + const dropdownBtn = document.getElementById('download-client-dropdown-btn'); + const dropdown = document.getElementById('download-client-dropdown'); + const selectAllBtn = document.getElementById('download-client-select-all'); + const deselectAllBtn = document.getElementById('download-client-deselect-all'); + + if (dropdownBtn && dropdown) { + // Toggle dropdown + dropdownBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const isOpen = dropdown.classList.toggle('open'); + dropdownBtn.classList.toggle('open', isOpen); + dropdownBtn.setAttribute('aria-expanded', isOpen); + }); + + // Close dropdown when clicking outside + document.addEventListener('click', (e) => { + if (!dropdown.contains(e.target) && !dropdownBtn.contains(e.target)) { + dropdown.classList.remove('open'); + dropdownBtn.classList.remove('open'); + dropdownBtn.setAttribute('aria-expanded', 'false'); + } + }); + + // Close dropdown on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + dropdown.classList.remove('open'); + dropdownBtn.classList.remove('open'); + dropdownBtn.setAttribute('aria-expanded', 'false'); + } + }); + } + + if (selectAllBtn) { + selectAllBtn.addEventListener('click', (e) => { + e.stopPropagation(); + selectedDownloadClients = downloadClients.map(c => c.id); + localStorage.setItem('sofarr-download-clients', JSON.stringify(selectedDownloadClients)); + updateDownloadClientFilter(); + renderDownloads(); + }); + } + + if (deselectAllBtn) { + deselectAllBtn.addEventListener('click', (e) => { + e.stopPropagation(); + selectedDownloadClients = []; + localStorage.setItem('sofarr-download-clients', JSON.stringify(selectedDownloadClients)); + updateDownloadClientFilter(); renderDownloads(); }); } } function updateDownloadClientFilter() { - const filterSelect = document.getElementById('download-client-filter'); - if (!filterSelect || downloadClients.length === 0) return; + const optionsContainer = document.getElementById('download-client-options'); + if (!optionsContainer) return; - // Save current selection - const currentValue = filterSelect.value; + // Clear existing options + optionsContainer.innerHTML = ''; - // Clear existing options (except "All clients") - filterSelect.innerHTML = ''; + if (downloadClients.length === 0) { + optionsContainer.innerHTML = '
No clients available
'; + return; + } - // Add options for each download client + // Add checkboxes for each download client downloadClients.forEach(client => { - const option = document.createElement('option'); - option.value = client.id; - option.textContent = client.name; - filterSelect.appendChild(option); + const option = document.createElement('div'); + option.className = 'download-client-option'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'download-client-checkbox'; + checkbox.value = client.id; + checkbox.checked = selectedDownloadClients.includes(client.id); + checkbox.id = `client-${client.id}`; + + const label = document.createElement('label'); + label.className = 'download-client-option-label'; + label.htmlFor = `client-${client.id}`; + label.textContent = client.name; + + const typeBadge = document.createElement('span'); + typeBadge.className = 'download-client-type'; + typeBadge.textContent = client.type; + + option.appendChild(checkbox); + option.appendChild(label); + option.appendChild(typeBadge); + + // Toggle selection when clicking the row + option.addEventListener('click', (e) => { + if (e.target !== checkbox) { + checkbox.checked = !checkbox.checked; + } + toggleClientSelection(client.id, checkbox.checked); + }); + + // Toggle selection when checkbox changes + checkbox.addEventListener('change', (e) => { + toggleClientSelection(client.id, e.target.checked); + }); + + optionsContainer.appendChild(option); }); - // Restore selection if still valid, otherwise default to 'all' - if (currentValue && (currentValue === 'all' || downloadClients.some(c => c.id === currentValue))) { - filterSelect.value = currentValue; + // Update button text + updateSelectedCountDisplay(); +} + +function toggleClientSelection(clientId, isSelected) { + if (isSelected) { + if (!selectedDownloadClients.includes(clientId)) { + selectedDownloadClients.push(clientId); + } } else { - filterSelect.value = 'all'; - selectedDownloadClient = 'all'; - localStorage.setItem('sofarr-download-client', 'all'); + selectedDownloadClients = selectedDownloadClients.filter(id => id !== clientId); + } + localStorage.setItem('sofarr-download-clients', JSON.stringify(selectedDownloadClients)); + updateSelectedCountDisplay(); + renderDownloads(); +} + +function updateSelectedCountDisplay() { + const selectedText = document.getElementById('download-client-selected-text'); + if (!selectedText) return; + + if (selectedDownloadClients.length === 0) { + selectedText.textContent = 'All clients'; + } else if (selectedDownloadClients.length === 1) { + const client = downloadClients.find(c => c.id === selectedDownloadClients[0]); + selectedText.textContent = client ? client.name : '1 selected'; + } else { + selectedText.textContent = `${selectedDownloadClients.length} selected`; } } diff --git a/public/index.html b/public/index.html index 35c8453..c30ab5a 100644 --- a/public/index.html +++ b/public/index.html @@ -147,9 +147,21 @@
- +
+ +
+
+ + +
+
+ +
+
+