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>
|
<button id="downloads-search-btn">🔍 Search</button>
|
||||||
</div>
|
</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 -->
|
<!-- Search Status Bar -->
|
||||||
<div class="search-status-container">
|
<div class="search-status-container">
|
||||||
<div class="spinner-animation hidden"></div>
|
<div class="spinner-animation hidden"></div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ let currentStream = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
track: null
|
track: null
|
||||||
};
|
};
|
||||||
|
let allSearchResults = [];
|
||||||
|
let currentFilterType = 'all';
|
||||||
|
let currentFilterFormat = 'all';
|
||||||
|
let currentSortBy = 'quality_score';
|
||||||
|
let isSortReversed = false;
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
const API = {
|
const API = {
|
||||||
|
|
@ -43,8 +48,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
initializeNavigation();
|
initializeNavigation();
|
||||||
initializeMediaPlayer();
|
initializeMediaPlayer();
|
||||||
initializeDonationWidget();
|
initializeDonationWidget();
|
||||||
initializeSettings();
|
|
||||||
initializeSearch();
|
|
||||||
|
|
||||||
// Start periodic updates
|
// Start periodic updates
|
||||||
updateServiceStatus();
|
updateServiceStatus();
|
||||||
|
|
@ -92,29 +96,32 @@ function navigateToPage(pageId) {
|
||||||
loadPageData(pageId);
|
loadPageData(pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REPLACE your old loadPageData function with this one:
|
||||||
|
// REPLACE your old loadPageData function with this corrected one
|
||||||
|
|
||||||
async function loadPageData(pageId) {
|
async function loadPageData(pageId) {
|
||||||
try {
|
try {
|
||||||
switch (pageId) {
|
switch (pageId) {
|
||||||
case 'dashboard':
|
case 'dashboard':
|
||||||
// Stop download polling when leaving downloads page
|
|
||||||
stopDownloadPolling();
|
stopDownloadPolling();
|
||||||
await loadDashboardData();
|
await loadDashboardData();
|
||||||
break;
|
break;
|
||||||
case 'sync':
|
case 'sync':
|
||||||
// Stop download polling when leaving downloads page
|
|
||||||
stopDownloadPolling();
|
stopDownloadPolling();
|
||||||
await loadSyncData();
|
await loadSyncData();
|
||||||
break;
|
break;
|
||||||
case 'downloads':
|
case 'downloads':
|
||||||
|
// --- FIX: Initialize first, THEN load data. This is the correct order. ---
|
||||||
|
initializeSearch();
|
||||||
|
initializeFilters();
|
||||||
await loadDownloadsData();
|
await loadDownloadsData();
|
||||||
break;
|
break;
|
||||||
case 'artists':
|
case 'artists':
|
||||||
// Stop download polling when leaving downloads page
|
|
||||||
stopDownloadPolling();
|
stopDownloadPolling();
|
||||||
await loadArtistsData();
|
await loadArtistsData();
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
// Stop download polling when leaving downloads page
|
initializeSettings();
|
||||||
stopDownloadPolling();
|
stopDownloadPolling();
|
||||||
await loadSettingsData();
|
await loadSettingsData();
|
||||||
break;
|
break;
|
||||||
|
|
@ -707,13 +714,16 @@ function browsePath(pathType) {
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
||||||
function initializeSearch() {
|
function initializeSearch() {
|
||||||
const searchInput = document.getElementById('search-input');
|
// --- FIX: Corrected the element IDs to match the HTML ---
|
||||||
const searchButton = document.getElementById('search-button');
|
const searchInput = document.getElementById('downloads-search-input');
|
||||||
|
const searchButton = document.getElementById('downloads-search-btn');
|
||||||
|
|
||||||
searchButton.addEventListener('click', performSearch);
|
if (searchButton && searchInput) {
|
||||||
searchInput.addEventListener('keypress', (e) => {
|
searchButton.addEventListener('click', performDownloadsSearch);
|
||||||
if (e.key === 'Enter') performSearch();
|
searchInput.addEventListener('keypress', (e) => {
|
||||||
});
|
if (e.key === 'Enter') performDownloadsSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performSearch() {
|
async function performSearch() {
|
||||||
|
|
@ -1230,11 +1240,14 @@ async function performDownloadsSearch() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = data.results || [];
|
const results = data.results || [];
|
||||||
displayDownloadsResults(results);
|
allSearchResults = results;
|
||||||
|
resetFilters();
|
||||||
|
applyFiltersAndSort();
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
showToast('No results found', 'error');
|
showToast('No results found', 'error');
|
||||||
} else {
|
} else {
|
||||||
|
document.getElementById('filters-container').classList.remove('hidden');
|
||||||
showToast(`Found ${results.length} results`, 'success');
|
showToast(`Found ${results.length} results`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1792,3 +1805,168 @@ window.downloadAlbumTrack = downloadAlbumTrack;
|
||||||
window.switchDownloadTab = switchDownloadTab;
|
window.switchDownloadTab = switchDownloadTab;
|
||||||
window.cancelDownloadItem = cancelDownloadItem;
|
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;
|
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 */
|
/* Sophisticated Download Tracking Styles */
|
||||||
|
|
||||||
|
|
@ -2507,3 +2483,96 @@ body {
|
||||||
.download-queue::-webkit-scrollbar-thumb:hover {
|
.download-queue::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(29, 185, 84, 0.5);
|
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