This commit is contained in:
Broque Thomas 2025-08-23 12:31:25 -07:00
parent f6cc9bffdb
commit 92656fbe25
3 changed files with 332 additions and 39 deletions

View file

@ -209,6 +209,52 @@
<button id="downloads-search-btn">🔍 Search</button>
</div>
<div id="filters-container" class="filters-container hidden">
<div class="filter-toggle-header">
<button id="filter-toggle-btn" class="filter-toggle-btn">⏷ Filters</button>
</div>
<div id="filter-content" class="filter-content hidden">
<!-- Filter by Type -->
<div class="filter-group">
<label class="filter-label">Type:</label>
<button class="filter-btn active" data-filter-type="type" data-value="all">All</button>
<button class="filter-btn" data-filter-type="type" data-value="album">Albums</button>
<button class="filter-btn" data-filter-type="type" data-value="track">Singles</button>
</div>
<!-- Filter by Format -->
<div class="filter-group">
<label class="filter-label">Format:</label>
<button class="filter-btn active" data-filter-type="format" data-value="all">All</button>
<button class="filter-btn" data-filter-type="format" data-value="flac">FLAC</button>
<button class="filter-btn" data-filter-type="format" data-value="mp3">MP3</button>
<!-- Added missing format buttons -->
<button class="filter-btn" data-filter-type="format" data-value="ogg">OGG</button>
<button class="filter-btn" data-filter-type="format" data-value="aac">AAC</button>
<button class="filter-btn" data-filter-type="format" data-value="wma">WMA</button>
</div>
<!-- Sort Controls -->
<div class="filter-group">
<label class="filter-label">Sort by:</label>
<button id="sort-order-btn" class="filter-btn sort-order-btn" data-order="desc"></button>
<!-- Added all sort options from the GUI -->
<button class="filter-btn active" data-filter-type="sort" data-value="relevance">Relevance</button>
<button class="filter-btn" data-filter-type="sort" data-value="quality_score">Quality</button>
<button class="filter-btn" data-filter-type="sort" data-value="size">Size</button>
<button class="filter-btn" data-filter-type="sort" data-value="title">Name</button>
<button class="filter-btn" data-filter-type="sort" data-value="username">Uploader</button>
<button class="filter-btn" data-filter-type="sort" data-value="bitrate">Bitrate</button>
<button class="filter-btn" data-filter-type="sort" data-value="duration">Duration</button>
<button class="filter-btn" data-filter-type="sort" data-value="availability">Available</button>
<button class="filter-btn" data-filter-type="sort" data-value="upload_speed">Speed</button>
</div>
</div>
</div>
<!-- Search Status Bar -->
<div class="search-status-container">
<div class="spinner-animation hidden"></div>

View file

@ -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;
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)

View file

@ -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);
}
}
/* 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 */