diff --git a/webui/index.html b/webui/index.html index db042022..13935d66 100644 --- a/webui/index.html +++ b/webui/index.html @@ -209,6 +209,52 @@ + + + +
diff --git a/webui/static/script.js b/webui/static/script.js index 83bb7b68..d328242d 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -12,6 +12,11 @@ let currentStream = { progress: 0, track: null }; +let allSearchResults = []; +let currentFilterType = 'all'; +let currentFilterFormat = 'all'; +let currentSortBy = 'quality_score'; +let isSortReversed = false; // API endpoints const API = { @@ -43,8 +48,7 @@ document.addEventListener('DOMContentLoaded', function() { initializeNavigation(); initializeMediaPlayer(); initializeDonationWidget(); - initializeSettings(); - initializeSearch(); + // Start periodic updates updateServiceStatus(); @@ -92,29 +96,32 @@ function navigateToPage(pageId) { loadPageData(pageId); } +// REPLACE your old loadPageData function with this one: +// REPLACE your old loadPageData function with this corrected one + async function loadPageData(pageId) { try { switch (pageId) { case 'dashboard': - // Stop download polling when leaving downloads page stopDownloadPolling(); await loadDashboardData(); break; case 'sync': - // Stop download polling when leaving downloads page stopDownloadPolling(); await loadSyncData(); break; case 'downloads': + // --- FIX: Initialize first, THEN load data. This is the correct order. --- + initializeSearch(); + initializeFilters(); await loadDownloadsData(); break; case 'artists': - // Stop download polling when leaving downloads page stopDownloadPolling(); await loadArtistsData(); break; case 'settings': - // Stop download polling when leaving downloads page + initializeSettings(); stopDownloadPolling(); await loadSettingsData(); break; @@ -707,13 +714,16 @@ function browsePath(pathType) { // =============================== function initializeSearch() { - const searchInput = document.getElementById('search-input'); - const searchButton = document.getElementById('search-button'); + // --- FIX: Corrected the element IDs to match the HTML --- + const searchInput = document.getElementById('downloads-search-input'); + const searchButton = document.getElementById('downloads-search-btn'); - searchButton.addEventListener('click', performSearch); - searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') performSearch(); - }); + if (searchButton && searchInput) { + searchButton.addEventListener('click', performDownloadsSearch); + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') performDownloadsSearch(); + }); + } } async function performSearch() { @@ -1230,11 +1240,14 @@ async function performDownloadsSearch() { } const results = data.results || []; - displayDownloadsResults(results); + allSearchResults = results; + resetFilters(); + applyFiltersAndSort(); if (results.length === 0) { showToast('No results found', 'error'); } else { + document.getElementById('filters-container').classList.remove('hidden'); showToast(`Found ${results.length} results`, 'success'); } @@ -1791,4 +1804,169 @@ window.toggleAlbumExpansion = toggleAlbumExpansion; window.downloadAlbumTrack = downloadAlbumTrack; window.switchDownloadTab = switchDownloadTab; window.cancelDownloadItem = cancelDownloadItem; -window.clearFinishedDownloads = clearFinishedDownloads; \ No newline at end of file +window.clearFinishedDownloads = clearFinishedDownloads; + +// APPEND THIS JAVASCRIPT SNIPPET (B) + +function initializeFilters() { + const toggleBtn = document.getElementById('filter-toggle-btn'); + const container = document.getElementById('filters-container'); + const content = document.getElementById('filter-content'); + + if (toggleBtn && container && content) { + // Using .onclick ensures we only ever have one click handler + toggleBtn.onclick = () => { + const isExpanded = container.classList.contains('expanded'); + + if (isExpanded) { + // Collapse the container + container.classList.remove('expanded'); + toggleBtn.textContent = '⏷ Filters'; + } else { + // Expand the container + content.classList.remove('hidden'); // Make sure content is visible for animation + container.classList.add('expanded'); + toggleBtn.textContent = '⏶ Filters'; + } + }; + } + + // This part is correct and doesn't need to change + document.querySelectorAll('.filter-btn').forEach(button => { + button.addEventListener('click', handleFilterClick); + }); +} + +function handleFilterClick(event) { + const button = event.target; + const filterType = button.dataset.filterType; + const value = button.dataset.value; + + if (filterType === 'type') currentFilterType = value; + if (filterType === 'format') currentFilterFormat = value; + if (filterType === 'sort') currentSortBy = value; + + if (button.id === 'sort-order-btn') { + isSortReversed = !isSortReversed; + button.textContent = isSortReversed ? '↑' : '↓'; + } + + document.querySelectorAll(`.filter-btn[data-filter-type="${filterType}"]`).forEach(btn => { + btn.classList.remove('active'); + }); + if (filterType) { // Don't try to activate the sort order button + button.classList.add('active'); + } + + applyFiltersAndSort(); +} + +function resetFilters() { + currentFilterType = 'all'; + currentFilterFormat = 'all'; + currentSortBy = 'quality_score'; + isSortReversed = false; + + document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active')); + document.querySelector('.filter-btn[data-filter-type="type"][data-value="all"]').classList.add('active'); + document.querySelector('.filter-btn[data-filter-type="format"][data-value="all"]').classList.add('active'); + document.querySelector('.filter-btn[data-filter-type="sort"][data-value="quality_score"]').classList.add('active'); + document.getElementById('sort-order-btn').textContent = '↓'; +} + +function applyFiltersAndSort() { + let processedResults = [...allSearchResults]; + const query = document.getElementById('downloads-search-input').value.trim().toLowerCase(); + + // 1. Filter by Type + if (currentFilterType !== 'all') { + processedResults = processedResults.filter(r => r.result_type === currentFilterType); + } + + // 2. Filter by Format + if (currentFilterFormat !== 'all') { + processedResults = processedResults.filter(r => { + const quality = (r.dominant_quality || r.quality || '').toLowerCase(); + return quality === currentFilterFormat; + }); + } + + // 3. Sort Results + processedResults.sort((a, b) => { + let valA, valB; + + // Special handling for relevance sort + if (currentSortBy === 'relevance') { + valA = calculateRelevanceScore(a, query); + valB = calculateRelevanceScore(b, query); + return valB - valA; // Higher score is better + } + + // Special handling for availability + if (currentSortBy === 'availability') { + valA = (a.free_upload_slots || 0) - (a.queue_length || 0) * 0.1; + valB = (b.free_upload_slots || 0) - (b.queue_length || 0) * 0.1; + return valB - valA; + } + + valA = a[currentSortBy] || 0; + valB = b[currentSortBy] || 0; + + if (typeof valA === 'string') { + // For name/title sort, use the correct property + const titleA = (a.album_title || a.title || '').toLowerCase(); + const titleB = (b.album_title || b.title || '').toLowerCase(); + return titleA.localeCompare(titleB); + } + + // Default numeric sort (descending) + return valB - valA; + }); + + // Handle sort direction toggle + const sortDefaults = { + relevance: 'desc', quality_score: 'desc', size: 'desc', bitrate: 'desc', + upload_speed: 'desc', duration: 'desc', availability: 'desc', + title: 'asc', username: 'asc' + }; + + const defaultOrder = sortDefaults[currentSortBy] || 'desc'; + if ((defaultOrder === 'asc' && isSortReversed) || (defaultOrder === 'desc' && !isSortReversed)) { + processedResults.reverse(); + } + + displayDownloadsResults(processedResults); +} + +function calculateRelevanceScore(result, query) { + let score = 0.0; + const queryTerms = query.split(' ').filter(t => t.length > 1); + + // 1. Search Term Matching (40%) + let searchableText = `${result.title || ''} ${result.artist || ''} ${result.album || ''} ${result.album_title || ''}`.toLowerCase(); + let termMatches = 0; + for (const term of queryTerms) { + if (searchableText.includes(term)) { + termMatches++; + } + } + score += (termMatches / queryTerms.length) * 0.40; + + // 2. Quality Score (25%) + score += (result.quality_score || 0) * 0.25; + + // 3. User Reliability (Availability & Speed) (20%) + const reliability = ((result.free_upload_slots || 0) > 0 ? 0.5 : 0) + Math.min(1, (result.upload_speed || 0) / 500) * 0.5; + score += reliability * 0.20; + + // 4. File Completeness (Bitrate & Duration) (15%) + const completeness = (Math.min(1, (result.bitrate || 0) / 320) * 0.5) + (result.duration > 0 ? 0.5 : 0); + score += completeness * 0.15; + + return score; +} + +// Add to global scope for onclick +window.handleFilterClick = handleFilterClick; + +// END OF JAVASCRIPT SNIPPET (B) \ No newline at end of file diff --git a/webui/static/style.css b/webui/static/style.css index a2e1c898..8185a6ab 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -2264,30 +2264,6 @@ body { margin-left: 8px; } -.track-download-btn { - background: linear-gradient(to bottom, - rgba(29, 185, 84, 0.8), - rgba(24, 156, 71, 0.7)); - border: 1px solid rgba(29, 185, 84, 0.4); - border-radius: 50%; - color: #000000; - font-size: 12px; - font-weight: bold; - width: 28px; - height: 28px; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.track-download-btn:hover { - background: linear-gradient(to bottom, - rgba(30, 215, 96, 0.9), - rgba(25, 180, 80, 0.8)); - border-color: rgba(29, 185, 84, 0.6); -} /* Sophisticated Download Tracking Styles */ @@ -2506,4 +2482,97 @@ body { .download-queue::-webkit-scrollbar-thumb:hover { background: rgba(29, 185, 84, 0.5); -} \ No newline at end of file +} + +/* APPEND THIS CSS SNIPPET */ + +/* ======================================================= */ +/* == STYLES FOR FILTERS PANEL == */ +/* ======================================================= */ + +.filters-container { + background: linear-gradient(to bottom, rgba(45, 45, 45, 0.6), rgba(35, 35, 35, 0.8)); + border-radius: 10px; + border: 1px solid rgba(80, 80, 80, 0.25); + padding: 8px 16px; + transition: max-height 0.4s ease-in-out; + max-height: fit-content; /* Collapsed height */ + margin-top: 16px; +} + +.filters-container.expanded { + max-height: fit-content; /* Expanded height */ + +} +.filters-container:not(.expanded) .filter-content { + display: none; +} + +.filter-toggle-header { + display: flex; +} + +.filter-toggle-btn { + background: linear-gradient(to bottom, rgba(80, 80, 80, 0.9), rgba(70, 70, 70, 0.95)); + border: 1px solid rgba(100, 100, 100, 0.3); + border-radius: 6px; + color: rgba(255, 255, 255, 0.8); + font-size: 11px; + font-weight: 600; + padding: 6px 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-toggle-btn:hover { + background: linear-gradient(to bottom, rgba(90, 90, 90, 0.9), rgba(80, 80, 80, 0.95)); +} + +.filter-content { + padding-top: 10px; +} + +.filter-group { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.filter-label { + color: rgba(255, 255, 255, 0.8); + font-size: 11px; + font-weight: 600; +} + +.filter-btn { + background: linear-gradient(to bottom, rgba(80, 80, 80, 0.4), rgba(60, 60, 60, 0.6)); + border: 1px solid rgba(120, 120, 120, 0.3); + border-radius: 16px; + color: rgba(255, 255, 255, 0.8); + font-size: 11px; + font-weight: 500; + padding: 4px 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-btn:hover { + background: linear-gradient(to bottom, rgba(100, 100, 100, 0.5), rgba(80, 80, 80, 0.7)); + border-color: rgba(140, 140, 140, 0.4); +} + +.filter-btn.active { + background: linear-gradient(to bottom, #1ed760, #1db954); + border-color: transparent; + color: #000000; + font-weight: 700; +} + +.sort-order-btn { + width: 28px; + height: 28px; + padding: 0; + font-size: 14px; +} +/* END OF CSS SNIPPET */ \ No newline at end of file