Add multi-select download client filter with client type display
All checks were successful
All checks were successful
This commit is contained in:
176
public/app.js
176
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 = '<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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,21 @@
|
||||
<div class="downloads-header">
|
||||
<div class="downloads-controls">
|
||||
<label class="download-client-label" for="download-client-filter">Download client:</label>
|
||||
<select id="download-client-filter" class="download-client-select">
|
||||
<option value="all">All clients</option>
|
||||
</select>
|
||||
<div class="download-client-filter" id="download-client-filter">
|
||||
<button class="download-client-dropdown-btn" id="download-client-dropdown-btn" type="button" aria-expanded="false">
|
||||
<span id="download-client-selected-text">All clients</span>
|
||||
<span class="dropdown-arrow">▼</span>
|
||||
</button>
|
||||
<div class="download-client-dropdown" id="download-client-dropdown">
|
||||
<div class="download-client-dropdown-header">
|
||||
<button class="download-client-dropdown-btn-small" id="download-client-select-all" type="button">Select All</button>
|
||||
<button class="download-client-dropdown-btn-small" id="download-client-deselect-all" type="button">Deselect All</button>
|
||||
</div>
|
||||
<div class="download-client-options" id="download-client-options">
|
||||
<!-- Options will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="no-downloads" class="no-downloads" style="display: none;">
|
||||
|
||||
144
public/style.css
144
public/style.css
@@ -698,6 +698,150 @@ body {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Multi-select dropdown container */
|
||||
.download-client-filter {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn .dropdown-arrow {
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn.open .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.download-client-count {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Dropdown panel */
|
||||
.download-client-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 4px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.download-client-dropdown.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Dropdown header with Select All/Deselect All buttons */
|
||||
.download-client-dropdown-header {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--surface);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn-small {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
background: var(--surface-alt);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.download-client-dropdown-btn-small:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Client option row */
|
||||
.download-client-option {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.download-client-option:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.download-client-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.download-client-option-label {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.download-client-type {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface-alt);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.download-client-empty {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user