Add multi-select download client filter with client type display
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 45s
CI / Security audit (push) Successful in 1m23s
CI / Tests & coverage (push) Successful in 1m35s

This commit is contained in:
2026-05-19 23:41:43 +01:00
parent 7195a09562
commit be791ed044
3 changed files with 307 additions and 31 deletions

View File

@@ -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 = '<option value="all">All clients</option>';
if (downloadClients.length === 0) {
optionsContainer.innerHTML = '<div class="download-client-empty">No clients available</div>';
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`;
}
}