diff --git a/webui/index.html b/webui/index.html index 3f2b1624..52788f0f 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2061,6 +2061,30 @@
+ +
+
+ Show + + + +
+
+
+ Include + + + +
+
+
+ Status + + + +
+
+
diff --git a/webui/static/script.js b/webui/static/script.js index 8a1d32c6..4de18d1d 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -27085,6 +27085,13 @@ let artistDetailPageState = { currentArtistName: null }; +// Discography filter state +let discographyFilterState = { + categories: { albums: true, eps: true, singles: true }, + content: { live: true, compilations: true, featured: true }, + ownership: 'all' // 'all', 'owned', 'missing' +}; + function navigateToArtistDetail(artistId, artistName) { console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`); @@ -27140,6 +27147,9 @@ function initializeArtistDetailPage() { }); } + // Initialize discography filter buttons + initializeDiscographyFilters(); + artistDetailPageState.isInitialized = true; console.log("✅ Artist Detail page initialized successfully"); } @@ -27147,6 +27157,9 @@ function initializeArtistDetailPage() { async function loadArtistDetailData(artistId, artistName) { console.log(`🔄 Loading artist detail data for: ${artistName} (ID: ${artistId})`); + // Reset discography filters to defaults + resetDiscographyFilters(); + // Show loading state and hide all content showArtistDetailLoading(true); showArtistDetailError(false); @@ -27491,6 +27504,9 @@ function populateDiscographySections(discography) { // Populate singles populateReleaseSection('singles', discography.singles); + + // Apply any active filters after populating + applyDiscographyFilters(); } function populateReleaseSection(sectionType, releases) { @@ -27540,6 +27556,18 @@ function createReleaseCard(release) { // Store mutable reference so stream updates propagate to click handler card._releaseData = release; + // Tag card for content-type filtering + const titleLower = (release.title || '').toLowerCase(); + const livePattern = /\b(live)\b|\(live[^)]*\)|\[live[^]]*\]/i; + const compilationPattern = /\b(greatest hits|best of|collection|anthology|essential)\b/i; + const featuredPattern = /\(?\bfeat\.?\s|\bft\.?\s|\bfeaturing\b/i; + const isLive = livePattern.test(release.title || '') || (release.album_type === 'compilation' && livePattern.test(release.title || '')); + const isCompilation = (release.album_type === 'compilation') || compilationPattern.test(release.title || ''); + const isFeatured = featuredPattern.test(release.title || ''); + card.setAttribute("data-is-live", isLive ? "true" : "false"); + card.setAttribute("data-is-compilation", isCompilation ? "true" : "false"); + card.setAttribute("data-is-featured", isFeatured ? "true" : "false"); + // Add MusicBrainz icon if available let mbIcon = null; if (release.musicbrainz_release_id) { @@ -27969,6 +27997,9 @@ function updateLibraryReleaseCard(data) { formatsDiv.innerHTML = data.formats.map(f => `${f}`).join(''); card.appendChild(formatsDiv); } + + // Re-apply filters so newly resolved cards respect active filters + applyDiscographyFilters(); } function updateCategoryStatsFromStream(category, ownedCount, missingCount) { @@ -28066,6 +28097,125 @@ function recalculateSummaryStats(artistFormatSet) { } } +// =============================================== +// Discography Filter Functions +// =============================================== + +function initializeDiscographyFilters() { + const container = document.getElementById('discography-filters'); + if (!container) return; + + container.addEventListener('click', (e) => { + const btn = e.target.closest('.discography-filter-btn'); + if (!btn) return; + + const filterType = btn.dataset.filter; + const value = btn.dataset.value; + + if (filterType === 'category') { + // Multi-toggle: toggle this category on/off + btn.classList.toggle('active'); + discographyFilterState.categories[value] = btn.classList.contains('active'); + } else if (filterType === 'content') { + // Multi-toggle: toggle this content type on/off + btn.classList.toggle('active'); + discographyFilterState.content[value] = btn.classList.contains('active'); + } else if (filterType === 'ownership') { + // Single-select: deactivate siblings, activate this one + container.querySelectorAll('[data-filter="ownership"]').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + discographyFilterState.ownership = value; + } + + applyDiscographyFilters(); + }); +} + +function resetDiscographyFilters() { + discographyFilterState.categories = { albums: true, eps: true, singles: true }; + discographyFilterState.content = { live: true, compilations: true, featured: true }; + discographyFilterState.ownership = 'all'; + + // Reset button visual states + const container = document.getElementById('discography-filters'); + if (!container) return; + container.querySelectorAll('.discography-filter-btn').forEach(btn => { + const filterType = btn.dataset.filter; + const value = btn.dataset.value; + if (filterType === 'ownership') { + btn.classList.toggle('active', value === 'all'); + } else { + btn.classList.add('active'); + } + }); +} + +function applyDiscographyFilters() { + const categories = ['albums', 'eps', 'singles']; + + for (const cat of categories) { + const section = document.getElementById(`${cat}-section`); + if (!section) continue; + + // Category toggle — hide entire section + if (!discographyFilterState.categories[cat]) { + section.style.display = 'none'; + continue; + } + section.style.display = ''; + + // Filter individual cards within the section + const grid = document.getElementById(`${cat}-grid`); + if (!grid) continue; + + let visibleOwned = 0; + let visibleMissing = 0; + let visibleCount = 0; + + grid.querySelectorAll('.release-card').forEach(card => { + let hidden = false; + + // Content filters + if (!discographyFilterState.content.live && card.getAttribute('data-is-live') === 'true') { + hidden = true; + } + if (!discographyFilterState.content.compilations && card.getAttribute('data-is-compilation') === 'true') { + hidden = true; + } + if (!discographyFilterState.content.featured && card.getAttribute('data-is-featured') === 'true') { + hidden = true; + } + + // Ownership filter (only apply if card is not still checking) + if (!hidden && discographyFilterState.ownership !== 'all' && card._releaseData) { + const owned = card._releaseData.owned; + if (owned !== null) { // Don't hide cards still being checked + if (discographyFilterState.ownership === 'owned' && !owned) hidden = true; + if (discographyFilterState.ownership === 'missing' && owned) hidden = true; + } + } + + card.style.display = hidden ? 'none' : ''; + + // Count visible cards for stats + if (!hidden && card._releaseData) { + visibleCount++; + if (card._releaseData.owned === true) visibleOwned++; + else if (card._releaseData.owned === false) visibleMissing++; + } + }); + + // Update section stats to reflect filtered view + const ownedEl = document.getElementById(`${cat}-owned-count`); + const missingEl = document.getElementById(`${cat}-missing-count`); + if (ownedEl) ownedEl.textContent = `${visibleOwned} owned`; + if (missingEl) missingEl.textContent = `${visibleMissing} missing`; + + // Hide section entirely if all cards are hidden + section.style.display = visibleCount === 0 ? 'none' : ''; + } +} + // UI state management functions function showArtistDetailLoading(show) { const loadingElement = document.getElementById("artist-detail-loading"); diff --git a/webui/static/style.css b/webui/static/style.css index 5df434d8..b7361a04 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -13566,6 +13566,62 @@ body { letter-spacing: 0.5px; } +/* Discography Filters */ +.discography-filters { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0 20px; + flex-wrap: wrap; +} + +.filter-group { + display: flex; + align-items: center; + gap: 6px; +} + +.filter-label { + font-size: 11px; + font-weight: 600; + color: rgba(255, 255, 255, 0.35); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 4px; +} + +.filter-divider { + width: 1px; + height: 24px; + background: rgba(255, 255, 255, 0.1); + margin: 0 4px; +} + +.discography-filter-btn { + padding: 5px 14px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.4); + font-size: 12px; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; + cursor: pointer; + border-radius: 16px; + transition: all 0.2s ease-out; +} + +.discography-filter-btn:hover { + color: rgba(255, 255, 255, 0.8); + border-color: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.08); +} + +.discography-filter-btn.active { + color: #fff; + border-color: rgba(29, 185, 84, 0.5); + background: rgba(29, 185, 84, 0.15); +} + /* Discography Sections */ .discography-sections { display: flex;