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