From 44b5032f17eb958a59c62c00a7155f4a35d7233e Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 26 Feb 2026 10:17:53 -0800 Subject: [PATCH] Add discography filters to artist detail page Introduce interactive discography filters on the artist detail page. Adds filter UI markup (category, content, ownership) in index.html and styles in style.css. Implements filter state, initialization, reset and apply logic in script.js; tags release cards with data attributes (live/compilation/featured) using regex heuristics, updates visibility and per-section owned/missing counts, and re-applies filters when releases are updated. Integrates filter setup into page init and resets filters when loading artist data. --- webui/index.html | 24 +++++++ webui/static/script.js | 150 +++++++++++++++++++++++++++++++++++++++++ webui/static/style.css | 56 +++++++++++++++ 3 files changed, 230 insertions(+) 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;