filters
This commit is contained in:
parent
f6cc9bffdb
commit
92656fbe25
3 changed files with 332 additions and 39 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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 */
|
||||
Loading…
Reference in a new issue