From cf185907940a19a0a846f7db2ee588abcc89a0b7 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:00:17 -0700 Subject: [PATCH] Promote Watchlist and Wishlist from modals to full sidebar pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Watchlist and Wishlist are now proper sidebar pages with full design treatment matching the app's established visual language — glass containers, gradient headers, accent lines, card hover effects. Watchlist page: artist grid with sort (name/scan date/date added), search filter, last scan summary strip, live scan activity, batch selection, all existing sub-modals (artist config, global settings, artist detail slideout) preserved and working. Wishlist page: stats strip (album count, singles count, next cycle), category cards with mosaic backgrounds, track list with inline search filter, batch operations, download integration. Auto-processing detection on header button shows download progress modal when active. Header buttons rewired to navigate to pages. All refresh points updated to reinitialize pages instead of reopening modals. Timer/polling cleanup on page navigation. Artist detail overlay converted to fixed positioning. --- webui/index.html | 233 ++++++++++++ webui/static/helper.js | 1 + webui/static/script.js | 488 +++++++++++++++++++++++-- webui/static/style.css | 791 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 1472 insertions(+), 41 deletions(-) diff --git a/webui/index.html b/webui/index.html index cfafb3fd..d42fa59d 100644 --- a/webui/index.html +++ b/webui/index.html @@ -212,6 +212,16 @@ Artists + + + Watchlist + 0 + + + + Wishlist + 0 + Automations @@ -6401,6 +6411,229 @@ + + + + + + + + + Watchlist + + + 0 artists + Next Auto: -- + + + + + + + + + + Waiting... + + + + Waiting... + + + Current Track: + Waiting... + Recently Added: + + + + + + + + + + + Scan for New Releases + + + + Cancel Scan + + + + Update Similar Artists + + + + Global Settings + + + + + + ⚠️ + Global override is active — per-artist settings are being ignored during scans. + + + + + + Last scan: -- + + + + + + + + + + Name A-Z + Name Z-A + Oldest Scanned + Recently Scanned + Recently Added + + + + + + + + Select All + + + + Remove Selected + + + + + + + + + + + + + + Your watchlist is empty + Add artists from the Artists page to monitor them for new releases. + Browse Artists + + + + + + + + + + + + 🎵 + Wishlist + + + 0 tracks + Next Auto: -- + + + + + + Cleanup + + + + Clear All + + + + + + + + 0 + Album Tracks + + + + 0 + Singles + + + + Albums/EPs + Next Cycle + + + + + + + + + 💿 + Albums / EPs + 0 tracks + Next in Queue + + + + + + 🎵 + Singles + 0 tracks + Next in Queue + + + + + + + + ← Back + + + Select All + + Download Selection + + + + + + + + + 0 selected + + Remove Selected + + + + Loading tracks... + + + + + + + + + Your wishlist is empty + Failed downloads and tracks from watchlist scans will appear here automatically. + + + + diff --git a/webui/static/helper.js b/webui/static/helper.js index f193029f..5a98a179 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3602,6 +3602,7 @@ const WHATS_NEW = { '2.2': [ // --- April 15, 2026 --- { date: 'April 15, 2026' }, + { title: 'Watchlist & Wishlist Sidebar Pages', desc: 'Watchlist and Wishlist promoted from modals to full sidebar pages. All features preserved — artist grid, scan controls, batch operations, live activity, countdown timers, category cards with mosaic backgrounds. Header buttons now navigate to the pages', page: 'watchlist' }, { title: 'Picard-Style MusicBrainz Album Consistency', desc: 'Recording MBIDs now pulled from the matched release tracklist instead of independent searches. Batch-level artist name used for stable cache keys. Post-batch consistency pass rewrites album-level tags on all files to guarantee identical MusicBrainz IDs — prevents Navidrome album splits' }, { title: 'Fix Spotify API Leaking When Deezer/iTunes is Primary', desc: 'Spotify was being called for watchlist album scanning, similar artist discovery, repair jobs, and the Artists page search even when another source was set as primary. All data-fetching now respects the configured primary source. Spotify playlist sync is unaffected' }, { title: 'Fix OAuth Callback Port Hardcoding', desc: 'Custom callback ports (SOULSYNC_SPOTIFY_CALLBACK_PORT / SOULSYNC_TIDAL_CALLBACK_PORT) are now respected in auth instruction pages and log messages instead of always showing 8888. Added startup diagnostics logging for callback port binding' }, diff --git a/webui/static/script.js b/webui/static/script.js index 4bf44b23..7e050bc0 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -33,6 +33,10 @@ let wishlistCountInterval = null; let wishlistCountdownInterval = null; // Countdown timer for wishlist overview modal let watchlistCountdownInterval = null; // Countdown timer for watchlist overview modal +// Page state for Watchlist & Wishlist sidebar pages +let watchlistPageState = { isInitialized: false, artists: [] }; +let wishlistPageState = { isInitialized: false }; + // --- Add these globals for the Sync Page --- let spotifyPlaylists = []; let selectedPlaylists = new Set(); @@ -2795,16 +2799,43 @@ function initializeMobileNavigation() { } function initializeWatchlist() { - // Add watchlist button click handler + // Watchlist button navigates to watchlist page const watchlistButton = document.getElementById('watchlist-button'); if (watchlistButton) { - watchlistButton.addEventListener('click', showWatchlistModal); + watchlistButton.addEventListener('click', () => navigateToPage('watchlist')); } - // Add wishlist button click handler (global init so it works on all pages) + // Wishlist button: check for active download process first, otherwise navigate to page const wishlistButton = document.getElementById('wishlist-button'); if (wishlistButton) { - wishlistButton.addEventListener('click', handleWishlistButtonClick); + wishlistButton.addEventListener('click', async () => { + try { + const resp = await fetch('/api/active-processes'); + if (resp.ok) { + const data = await resp.json(); + const serverProcess = (data.active_processes || []).find(p => p.playlist_id === 'wishlist'); + if (serverProcess) { + // Active wishlist download — show the download progress modal + WishlistModalState.clearUserClosed(); + const clientProcess = activeDownloadProcesses['wishlist']; + const needsRehydration = !clientProcess || + clientProcess.batchId !== serverProcess.batch_id || + !clientProcess.modalElement || + !document.body.contains(clientProcess.modalElement); + if (needsRehydration) { + await rehydrateModal(serverProcess, true); + } else { + clientProcess.modalElement.style.display = 'flex'; + WishlistModalState.setVisible(); + } + return; + } + } + } catch (e) { + console.debug('Could not check active processes:', e); + } + navigateToPage('wishlist'); + }); } // Update watchlist count initially @@ -2920,6 +2951,9 @@ async function loadPageData(pageId) { stopDbUpdatePolling(); stopWishlistCountPolling(); stopLogPolling(); + // Stop watchlist/wishlist page timers when navigating away + if (watchlistCountdownInterval) { clearInterval(watchlistCountdownInterval); watchlistCountdownInterval = null; } + if (wishlistCountdownInterval) { clearInterval(wishlistCountdownInterval); wishlistCountdownInterval = null; } switch (pageId) { case 'dashboard': await loadDashboardData(); @@ -3014,6 +3048,12 @@ async function loadPageData(pageId) { // Load comparisons loadHydrabaseComparisons(); break; + case 'watchlist': + await initializeWatchlistPage(); + break; + case 'wishlist': + await initializeWishlistPage(); + break; case 'automations': await loadAutomations(); break; @@ -3528,8 +3568,8 @@ let _wishlistAutoProcessingNotified = false; function updateWishlistStatsFromData(data) { // Auto-processing detection: close modal and notify (once only) if (data.is_auto_processing) { - if (!_wishlistAutoProcessingNotified && typeof closeWishlistOverviewModal === 'function') { - closeWishlistOverviewModal(); + if (!_wishlistAutoProcessingNotified) { + if (currentPage === 'wishlist') navigateToPage('active-downloads'); showToast('Wishlist auto-processing started. View progress in Download Manager.', 'info'); _wishlistAutoProcessingNotified = true; } @@ -13600,7 +13640,7 @@ function startWishlistCountdownTimer(currentCycle, initialSeconds) { const data = _lastWishlistStats; if (data.is_auto_processing) { if (!_wishlistAutoProcessingNotified) { - closeWishlistOverviewModal(); + navigateToPage('active-downloads'); showToast('Wishlist auto-processing started. View progress in Download Manager.', 'info'); _wishlistAutoProcessingNotified = true; } @@ -13713,13 +13753,14 @@ async function cleanupWishlistOverview() { const statsData = await statsResponse.json(); if (statsData.total === 0) { - // Wishlist is empty, just close the modal - closeWishlistOverviewModal(); + // Wishlist is empty, refresh the page to show empty state + wishlistPageState.isInitialized = false; + await initializeWishlistPage(); await updateWishlistCount(); } else { - // Wishlist still has items, refresh the modal to show updated counts - closeWishlistOverviewModal(); - await openWishlistOverviewModal(); + // Wishlist still has items, refresh the page to show updated counts + wishlistPageState.isInitialized = false; + await initializeWishlistPage(); } } else { showToast(`Failed to cleanup wishlist: ${result.error || 'Unknown error'}`, 'error'); @@ -13766,9 +13807,9 @@ async function clearEntireWishlist() { console.log('Updating wishlist button count...'); await updateWishlistCount(); - console.log('Closing modal...'); - closeWishlistOverviewModal(); - console.log('Modal should be closed now'); + console.log('Refreshing wishlist page...'); + wishlistPageState.isInitialized = false; + await initializeWishlistPage(); } else { console.error('Clear failed:', result.error); showToast(`Failed to clear wishlist: ${result.error || 'Unknown error'}`, 'error'); @@ -14071,11 +14112,13 @@ function backToCategories() { const categoryGrid = document.querySelector('.wishlist-category-grid'); const downloadBtn = document.getElementById('wishlist-download-btn'); const batchBar = document.getElementById('wishlist-batch-bar'); + const trackSearch = document.getElementById('wishlist-track-search-input'); categoryTracksSection.style.display = 'none'; categoryGrid.style.display = 'grid'; downloadBtn.style.display = 'none'; if (batchBar) batchBar.style.display = 'none'; + if (trackSearch) trackSearch.value = ''; window.selectedWishlistCategory = null; } @@ -14383,11 +14426,10 @@ async function downloadSelectedCategory() { return; } - // Collect checked track IDs BEFORE closing the modal (checkboxes are destroyed on close) + // Collect checked track IDs const checkedBoxes = document.querySelectorAll('.wishlist-select-cb:checked'); const selectedTrackIds = new Set(Array.from(checkedBoxes).map(cb => cb.dataset.trackId).filter(Boolean)); - closeWishlistOverviewModal(); await openDownloadMissingWishlistModal(category, selectedTrackIds.size > 0 ? selectedTrackIds : null); } @@ -39937,7 +39979,377 @@ async function updateArtistCardWatchlistStatus() { } /** - * Show watchlist modal + * Initialize/refresh the watchlist sidebar page + */ +async function initializeWatchlistPage() { + try { + const emptyEl = document.getElementById('watchlist-page-empty'); + const gridEl = document.getElementById('watchlist-artists-list'); + const countEl = document.getElementById('watchlist-page-count'); + const overrideBanner = document.getElementById('watchlist-page-override-banner'); + + // Fetch count, artists, scan status, global config in parallel + const [countRes, artistsRes, statusRes, globalRes] = await Promise.all([ + fetch('/api/watchlist/count').then(r => r.json()), + fetch('/api/watchlist/artists').then(r => r.json()), + fetch('/api/watchlist/scan/status').then(r => r.json()), + fetch('/api/watchlist/global-config').then(r => r.json()).catch(() => ({ success: false })), + ]); + + const count = countRes.success ? countRes.count : 0; + const artists = artistsRes.success ? artistsRes.artists : []; + const scanStatus = statusRes.success ? statusRes.status : 'idle'; + const globalOverrideActive = globalRes.success && globalRes.config && globalRes.config.global_override_enabled; + + // Update count + if (countEl) countEl.textContent = `${count} artist${count !== 1 ? 's' : ''}`; + + // Update nav badge + const navBadge = document.getElementById('watchlist-nav-badge'); + if (navBadge) { + navBadge.textContent = count; + navBadge.classList.toggle('hidden', count === 0); + } + + // Empty state + if (count === 0) { + if (emptyEl) emptyEl.style.display = ''; + if (gridEl) gridEl.style.display = 'none'; + watchlistPageState.isInitialized = true; + return; + } + if (emptyEl) emptyEl.style.display = 'none'; + if (gridEl) gridEl.style.display = ''; + + // Store artists for sorting + watchlistPageState.artists = artists; + + // Last scan summary strip + const scanStrip = document.getElementById('watchlist-last-scan-strip'); + const scanText = document.getElementById('watchlist-last-scan-text'); + if (scanStrip && scanText && statusRes.completed_at && statusRes.summary) { + const completedDate = new Date(statusRes.completed_at); + const ago = _formatTimeAgo(completedDate); + const found = statusRes.summary.new_tracks_found || 0; + const added = statusRes.summary.tracks_added_to_wishlist || 0; + scanText.textContent = `Last scan: ${ago} — ${found} new track${found !== 1 ? 's' : ''} found, ${added} added to wishlist`; + scanStrip.style.display = ''; + } else if (scanStrip) { + scanStrip.style.display = 'none'; + } + + // Global override banner + if (overrideBanner) overrideBanner.style.display = globalOverrideActive ? '' : 'none'; + const settingsBtn = document.getElementById('watchlist-page-settings-btn'); + if (settingsBtn) { + settingsBtn.classList.toggle('watchlist-global-settings-active', globalOverrideActive); + settingsBtn.innerHTML = ` ${globalOverrideActive ? 'Global Override ON' : 'Global Settings'}`; + } + + // Render artist cards + if (gridEl) { + gridEl.innerHTML = artists.map(artist => { + const pills = []; + if (artist.include_albums) pills.push('Albums'); + if (artist.include_eps) pills.push('EPs'); + if (artist.include_singles) pills.push('Singles'); + if (artist.include_live) pills.push('Live'); + if (artist.include_remixes) pills.push('Remixes'); + if (artist.include_acoustic) pills.push('Acoustic'); + if (artist.include_compilations) pills.push('Compilations'); + const sourceBadges = []; + if (artist.spotify_artist_id) sourceBadges.push('Spotify'); + if (artist.itunes_artist_id) sourceBadges.push('iTunes'); + if (artist.deezer_artist_id) sourceBadges.push('Deezer'); + if (artist.discogs_artist_id) sourceBadges.push('Discogs'); + const artistPrimaryId = artist.spotify_artist_id || artist.itunes_artist_id || artist.deezer_artist_id || artist.discogs_artist_id; + return ` + + + + + + + + + + ${artist.image_url ? `` : '🎤'} + + + ${escapeHtml(artist.artist_name)} + ${formatRelativeScanTime(artist.last_scan_timestamp)} + + ${sourceBadges.length > 0 ? `${sourceBadges.join('')}` : ''} + ${pills.length > 0 ? `${pills.join('')}` : ''} + + `; + }).join(''); + + // Wire up gear buttons + gridEl.querySelectorAll('.watchlist-card-gear').forEach(button => { + button.addEventListener('click', () => { + openWatchlistArtistConfigModal(button.getAttribute('data-artist-id'), button.getAttribute('data-artist-name')); + }); + }); + + // Wire up artist card clicks + gridEl.querySelectorAll('.watchlist-artist-card').forEach(item => { + item.addEventListener('click', (e) => { + if (e.target.closest('.watchlist-card-gear') || e.target.closest('.watchlist-card-checkbox')) return; + const artistId = item.getAttribute('data-artist-id'); + const artistName = item.querySelector('.watchlist-card-name').textContent; + openWatchlistArtistDetailView(artistId, artistName); + }); + }); + } + + // Scan status + const scanStatusEl = document.getElementById('watchlist-scan-status'); + const liveActivityEl = document.getElementById('watchlist-live-activity'); + const scanBtn = document.getElementById('scan-watchlist-btn'); + const cancelBtn = document.getElementById('cancel-watchlist-scan-btn'); + + if (scanStatus === 'scanning') { + if (scanStatusEl) scanStatusEl.style.display = ''; + if (liveActivityEl) liveActivityEl.style.display = 'flex'; + if (scanBtn) { scanBtn.disabled = true; scanBtn.classList.add('btn-processing'); scanBtn.innerHTML = ' Scanning...'; } + if (cancelBtn) cancelBtn.style.display = ''; + pollWatchlistScanStatus(); + } else { + if (scanStatusEl && statusRes.summary) { + scanStatusEl.style.display = ''; + const summaryEl = document.getElementById('watchlist-page-scan-summary'); + if (summaryEl) { + summaryEl.style.display = ''; + summaryEl.innerHTML = `Artists: ${statusRes.summary.total_artists || 0} • New tracks: ${statusRes.summary.new_tracks_found || 0} • Added to wishlist: ${statusRes.summary.tracks_added_to_wishlist || 0}`; + } + } + } + + // Start countdown timer + const nextRunSeconds = countRes.next_run_in_seconds || 0; + startWatchlistCountdownTimer(nextRunSeconds); + + watchlistPageState.isInitialized = true; + + } catch (error) { + console.error('Error initializing watchlist page:', error); + showToast('Failed to load watchlist', 'error'); + } +} + +/** + * Initialize/refresh the wishlist sidebar page + */ +async function initializeWishlistPage() { + try { + const emptyEl = document.getElementById('wishlist-page-empty'); + const categoriesEl = document.getElementById('wishlist-page-categories'); + const countEl = document.getElementById('wishlist-page-count'); + const tracksSection = document.getElementById('wishlist-category-tracks'); + + // Fetch stats and cycle + const [statsRes, cycleRes] = await Promise.all([ + fetch('/api/wishlist/stats').then(r => r.json()), + fetch('/api/wishlist/cycle').then(r => r.json()), + ]); + + const { singles = 0, albums = 0, total = 0 } = statsRes; + const currentCycle = cycleRes.cycle || 'albums'; + + // Update count + if (countEl) countEl.textContent = `${total} track${total !== 1 ? 's' : ''}`; + + // Update nav badge + const navBadge = document.getElementById('wishlist-nav-badge'); + if (navBadge) { + navBadge.textContent = total; + navBadge.classList.toggle('hidden', total === 0); + } + + // Stats strip + const statAlbums = document.getElementById('wishlist-stat-albums'); + const statSingles = document.getElementById('wishlist-stat-singles'); + const statCycle = document.getElementById('wishlist-stat-cycle'); + const statsStrip = document.getElementById('wishlist-stats-strip'); + if (statAlbums) statAlbums.textContent = albums; + if (statSingles) statSingles.textContent = singles; + if (statCycle) statCycle.textContent = currentCycle === 'albums' ? 'Albums/EPs' : 'Singles'; + + // Empty state + if (total === 0) { + if (emptyEl) emptyEl.style.display = ''; + if (categoriesEl) categoriesEl.style.display = 'none'; + if (tracksSection) tracksSection.style.display = 'none'; + if (statsStrip) statsStrip.style.display = 'none'; + wishlistPageState.isInitialized = true; + return; + } + if (emptyEl) emptyEl.style.display = 'none'; + if (categoriesEl) categoriesEl.style.display = ''; + if (statsStrip) statsStrip.style.display = ''; + + // Reset to category view + if (tracksSection) tracksSection.style.display = 'none'; + if (categoriesEl) categoriesEl.style.display = ''; + + // Update category cards + const albumsCountEl = document.getElementById('wishlist-page-albums-count'); + const singlesCountEl = document.getElementById('wishlist-page-singles-count'); + const albumsBadge = document.getElementById('wishlist-page-albums-badge'); + const singlesBadge = document.getElementById('wishlist-page-singles-badge'); + + if (albumsCountEl) albumsCountEl.textContent = `${albums} tracks`; + if (singlesCountEl) singlesCountEl.textContent = `${singles} tracks`; + if (albumsBadge) albumsBadge.style.display = currentCycle === 'albums' ? '' : 'none'; + if (singlesBadge) singlesBadge.style.display = currentCycle === 'singles' ? '' : 'none'; + + // Add/remove next-in-queue class + const albumCard = categoriesEl.querySelector('[data-category="albums"]'); + const singleCard = categoriesEl.querySelector('[data-category="singles"]'); + if (albumCard) albumCard.classList.toggle('next-in-queue', currentCycle === 'albums'); + if (singleCard) singleCard.classList.toggle('next-in-queue', currentCycle === 'singles'); + + // Load mosaic covers + try { + const [albumTracksData, singleTracksData] = await Promise.all([ + fetch('/api/wishlist/tracks?category=albums&limit=50').then(r => r.json()), + fetch('/api/wishlist/tracks?category=singles&limit=50').then(r => r.json()), + ]); + const albumCovers = extractUniqueCoverImages(albumTracksData.tracks || [], 20); + const singleCovers = extractUniqueCoverImages(singleTracksData.tracks || [], 20); + + const albumsMosaic = document.getElementById('wishlist-page-albums-mosaic'); + const singlesMosaic = document.getElementById('wishlist-page-singles-mosaic'); + if (albumsMosaic) albumsMosaic.innerHTML = generateMosaicBackground(albumCovers); + if (singlesMosaic) singlesMosaic.innerHTML = generateMosaicBackground(singleCovers); + } catch (e) { + console.debug('Could not load mosaic covers:', e); + } + + // Start countdown timer + const nextRunSeconds = statsRes.next_run_in_seconds || 0; + const nextCycleText = currentCycle === 'albums' ? 'Albums/EPs' : 'Singles'; + startWishlistCountdownTimer(currentCycle, nextRunSeconds); + + wishlistPageState.isInitialized = true; + + } catch (error) { + console.error('Error initializing wishlist page:', error); + showToast('Failed to load wishlist', 'error'); + } +} + +/** + * Sort the watchlist artist grid by the selected criteria. + */ +function sortWatchlistArtists(sortBy) { + const grid = document.getElementById('watchlist-artists-list'); + if (!grid) return; + const cards = Array.from(grid.querySelectorAll('.watchlist-artist-card')); + if (cards.length === 0) return; + + cards.sort((a, b) => { + switch (sortBy) { + case 'name-asc': + return (a.dataset.artistName || '').localeCompare(b.dataset.artistName || ''); + case 'name-desc': + return (b.dataset.artistName || '').localeCompare(a.dataset.artistName || ''); + case 'scan-oldest': { + const aTime = a.dataset.lastScan ? new Date(a.dataset.lastScan).getTime() : 0; + const bTime = b.dataset.lastScan ? new Date(b.dataset.lastScan).getTime() : 0; + return aTime - bTime; // oldest first (never scanned = 0 = top) + } + case 'scan-newest': { + const aTime = a.dataset.lastScan ? new Date(a.dataset.lastScan).getTime() : 0; + const bTime = b.dataset.lastScan ? new Date(b.dataset.lastScan).getTime() : 0; + return bTime - aTime; + } + case 'added-newest': { + const aTime = a.dataset.added ? new Date(a.dataset.added).getTime() : 0; + const bTime = b.dataset.added ? new Date(b.dataset.added).getTime() : 0; + return bTime - aTime; + } + default: + return 0; + } + }); + + // Re-append in sorted order (preserves event listeners) + cards.forEach(card => grid.appendChild(card)); +} + +/** + * Filter wishlist tracks by search query within the active track list. + */ +function filterWishlistTracks() { + const input = document.getElementById('wishlist-track-search-input'); + if (!input) return; + const query = input.value.toLowerCase().trim(); + const tracksList = document.getElementById('wishlist-tracks-list'); + if (!tracksList) return; + + // For albums view: filter album cards by album name or track names within + const albumCards = tracksList.querySelectorAll('.wishlist-album-card'); + if (albumCards.length > 0) { + albumCards.forEach(card => { + const albumHeader = card.querySelector('.wishlist-album-header'); + const albumName = (albumHeader?.querySelector('.wishlist-album-name')?.textContent || '').toLowerCase(); + const artistName = (albumHeader?.querySelector('.wishlist-album-artist')?.textContent || '').toLowerCase(); + const tracks = card.querySelectorAll('.wishlist-album-track'); + let albumHasMatch = !query || albumName.includes(query) || artistName.includes(query); + + // Also check individual track names + if (!albumHasMatch && tracks.length > 0) { + tracks.forEach(track => { + const trackName = (track.textContent || '').toLowerCase(); + if (trackName.includes(query)) albumHasMatch = true; + }); + } + + card.style.display = albumHasMatch ? '' : 'none'; + }); + return; + } + + // For singles view: filter individual track rows + const trackRows = tracksList.querySelectorAll('.playlist-track-item-with-image, .playlist-track-item'); + trackRows.forEach(row => { + const text = (row.textContent || '').toLowerCase(); + row.style.display = (!query || text.includes(query)) ? '' : 'none'; + }); +} + +/** + * Format a Date object as a relative time string (e.g. "2 hours ago") + */ +function _formatTimeAgo(date) { + const now = new Date(); + const diffMs = now - date; + const diffMins = Math.floor(diffMs / 60000); + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h ago`; + const diffDays = Math.floor(diffHours / 24); + if (diffDays === 1) return 'yesterday'; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} + +/** + * Show watchlist modal (legacy — kept for backward compatibility) */ async function showWatchlistModal() { try { @@ -40725,9 +41137,8 @@ async function openWatchlistArtistDetailView(artistId, artistName) { source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : 'itunes'; } if (discogId) { - // Close watchlist modal + detail overlay + // Close detail overlay and navigate to Artists page closeWatchlistArtistDetailView(); - closeWatchlistModal(); // Navigate to Artists page and load discography navigateToPage('artists'); setTimeout(() => { @@ -40750,13 +41161,10 @@ async function openWatchlistArtistDetailView(artistId, artistName) { removeFromWatchlistModal(artistId, artistName); }); - // Append to the modal container - const container = document.querySelector('.watchlist-fullscreen'); - if (container) { - container.appendChild(overlay); - // Trigger slide-in animation - requestAnimationFrame(() => overlay.classList.add('visible')); - } + // Append to body as a fixed overlay + document.body.appendChild(overlay); + // Trigger slide-in animation + requestAnimationFrame(() => overlay.classList.add('visible')); } catch (error) { console.error('Error opening artist detail view:', error); @@ -40932,10 +41340,10 @@ async function saveWatchlistGlobalConfig() { showToast('Global watchlist settings saved', 'success'); closeWatchlistGlobalSettingsModal(); - // Refresh the watchlist modal to update button and banner - const watchlistModal = document.getElementById('watchlist-modal'); - if (watchlistModal && watchlistModal.style.display === 'flex') { - await showWatchlistModal(); + // Refresh the watchlist page to update the grid + if (currentPage === 'watchlist') { + watchlistPageState.isInitialized = false; + await initializeWatchlistPage(); } } else { showToast(`Error: ${data.error}`, 'error'); @@ -41006,10 +41414,10 @@ async function saveWatchlistArtistConfig(artistId) { showToast('Artist preferences saved successfully', 'success'); closeWatchlistArtistConfigModal(); - // Refresh watchlist modal if it's open - const watchlistModal = document.getElementById('watchlist-modal'); - if (watchlistModal && watchlistModal.style.display === 'flex') { - await showWatchlistModal(); + // Refresh watchlist page if we're on it + if (currentPage === 'watchlist') { + watchlistPageState.isInitialized = false; + await initializeWatchlistPage(); } } else { showToast(`Error saving preferences: ${data.error}`, 'error'); @@ -41494,8 +41902,9 @@ async function removeFromWatchlistModal(artistId, artistName) { // Close detail view if open closeWatchlistArtistDetailView(); - // Refresh the modal - showWatchlistModal(); + // Refresh the watchlist page + watchlistPageState.isInitialized = false; + await initializeWatchlistPage(); // Update button count updateWatchlistButtonCount(); @@ -41585,8 +41994,9 @@ async function batchRemoveFromWatchlist() { console.log(`❌ Batch removed ${data.removed} artists from watchlist`); - // Refresh the modal - showWatchlistModal(); + // Refresh the watchlist page + watchlistPageState.isInitialized = false; + await initializeWatchlistPage(); // Update button count updateWatchlistButtonCount(); diff --git a/webui/static/style.css b/webui/static/style.css index 27072a2b..17a06e66 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -15564,13 +15564,13 @@ body.helper-mode-active #dashboard-activity-feed:hover { /* Watchlist Artist Detail Overlay */ .watchlist-artist-detail-overlay { - position: absolute; + position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(18, 18, 18, 0.98); - z-index: 10; + z-index: 1000; transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); overflow-y: auto; @@ -55796,3 +55796,790 @@ body.reduce-effects *::after { min-width: 70px; } } + +/* ═══════════════════════════════════════════════════════════════════ + WATCHLIST PAGE + ═══════════════════════════════════════════════════════════════════ */ + +.watchlist-page-container { + padding: 28px 24px 30px; + max-width: 1400px; + margin: 0 auto; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.55) 0%, + rgba(12, 12, 12, 0.62) 100%); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + position: relative; +} + +/* ── Header ── */ +.watchlist-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 20px 24px 18px; + margin: -28px -24px 20px -24px; + background: linear-gradient(180deg, + rgba(255, 193, 7, 0.08) 0%, + rgba(255, 193, 7, 0.03) 40%, + transparent 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-top-left-radius: 24px; + border-top-right-radius: 24px; + position: relative; +} + +.watchlist-page-header::after { + content: ''; + position: absolute; + bottom: -1px; + left: 10%; + right: 10%; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 193, 7, 0.2) 20%, + rgba(255, 193, 7, 0.35) 50%, + rgba(255, 193, 7, 0.2) 80%, + transparent 100%); +} + +.watchlist-page-header-left { + display: flex; + flex-direction: column; + gap: 6px; +} + +.watchlist-page-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 26px; + font-weight: 700; + color: #fff; + margin: 0; + letter-spacing: -0.5px; + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.watchlist-page-meta { + display: flex; + align-items: center; + gap: 16px; + font-size: 13px; + color: rgba(255, 255, 255, 0.45); + font-weight: 500; +} + +.watchlist-page-meta span { + display: flex; + align-items: center; + gap: 5px; +} + +.watchlist-page-count { + color: rgba(255, 193, 7, 0.8); + font-weight: 600; +} + +.watchlist-page-timer { + padding-left: 16px; + border-left: 1px solid rgba(255, 255, 255, 0.08); +} + +/* ── Action buttons ── */ +.watchlist-page-actions { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + margin-bottom: 20px; + padding: 0 4px; +} + +.watchlist-action-btn { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 9px 18px; + border-radius: 10px; + border: 1px solid transparent; + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + letter-spacing: 0.1px; +} + +.watchlist-action-primary { + background: linear-gradient(135deg, rgb(var(--accent-rgb)), rgba(var(--accent-rgb), 0.85)); + color: #fff; + border-color: rgba(var(--accent-rgb), 0.3); + box-shadow: 0 2px 8px rgba(var(--accent-rgb), 0.2); +} + +.watchlist-action-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 14px rgba(var(--accent-rgb), 0.3); + filter: brightness(1.1); +} + +.watchlist-action-primary:disabled { + opacity: 0.45; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.watchlist-action-secondary { + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.7); + border-color: rgba(255, 255, 255, 0.08); +} + +.watchlist-action-secondary:hover { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border-color: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); +} + +.watchlist-action-danger { + background: rgba(239, 68, 68, 0.1); + color: #f87171; + border-color: rgba(239, 68, 68, 0.15); +} + +.watchlist-action-danger:hover { + background: rgba(239, 68, 68, 0.18); + border-color: rgba(239, 68, 68, 0.25); + transform: translateY(-1px); +} + +/* ── Scan status card ── */ +.watchlist-page-scan-status { + display: flex; + flex-direction: column; + align-items: center; + padding: 18px 24px; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.95) 0%, + rgba(12, 12, 12, 0.98) 100%); + border-radius: 14px; + margin-bottom: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + position: relative; + overflow: hidden; +} + +.watchlist-page-scan-status::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.5), transparent); + opacity: 0; + transition: opacity 0.3s; +} + +.watchlist-page-scan-status:has(.watchlist-live-activity[style*="flex"])::before { + opacity: 1; + animation: scanPulse 2s ease-in-out infinite; +} + +@keyframes scanPulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +/* ── Last scan summary strip ── */ +.watchlist-last-scan-strip { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: linear-gradient(135deg, + rgba(255, 193, 7, 0.06) 0%, + rgba(255, 152, 0, 0.03) 100%); + border: 1px solid rgba(255, 193, 7, 0.1); + border-radius: 10px; + margin-bottom: 16px; + font-size: 13px; + color: rgba(255, 255, 255, 0.55); + font-weight: 500; +} + +.watchlist-last-scan-strip svg { + color: rgba(255, 193, 7, 0.6); + flex-shrink: 0; +} + +/* ── Toolbar (search + sort) ── */ +.watchlist-toolbar { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 16px; + padding: 0 4px; +} + +.watchlist-toolbar .watchlist-search-container { + flex: 1; + margin-bottom: 0; +} + +.watchlist-sort-select { + padding: 10px 32px 10px 14px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.65); + font-size: 13px; + font-weight: 500; + border-radius: 10px; + cursor: pointer; + transition: all 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.4)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + white-space: nowrap; +} + +.watchlist-sort-select:hover { + border-color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.85); +} + +.watchlist-sort-select:focus { + border-color: rgba(255, 193, 7, 0.4); + outline: none; + box-shadow: 0 0 12px rgba(255, 193, 7, 0.1); +} + +.watchlist-sort-select option { + background: #1a1a1a; + color: #fff; +} + +/* ── Search bar (page-level) ── */ +.watchlist-page-container .watchlist-search-container { + margin-bottom: 16px; + padding: 0; + position: relative; +} + +.watchlist-page-container .watchlist-search-input { + width: 100%; + padding: 12px 20px 12px 44px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); + color: #ffffff; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + box-sizing: border-box; +} + +.watchlist-page-container .watchlist-search-input:focus { + border-color: rgba(255, 193, 7, 0.5); + background: rgba(255, 255, 255, 0.06); + box-shadow: 0 0 16px rgba(255, 193, 7, 0.12); + outline: none; +} + +.watchlist-page-container .watchlist-search-icon { + left: 20px; +} + +/* ── Artist grid (page-level) ── */ +.watchlist-page-container .watchlist-artists-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + padding: 4px; +} + +/* ── Artist cards upgrade ── */ +.watchlist-page-container .watchlist-artist-card { + background: linear-gradient(135deg, + rgba(26, 26, 26, 0.95) 0%, + rgba(18, 18, 18, 0.98) 100%); + border: 1px solid rgba(255, 255, 255, 0.07); + border-top: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 14px; + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + contain: none; +} + +.watchlist-page-container .watchlist-artist-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, rgba(255, 193, 7, 0.4), transparent); + opacity: 0; + transition: opacity 0.3s; +} + +.watchlist-page-container .watchlist-artist-card:hover { + transform: translateY(-3px); + border-color: rgba(255, 193, 7, 0.15); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.4), + 0 0 20px rgba(255, 193, 7, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +.watchlist-page-container .watchlist-artist-card:hover::before { + opacity: 1; +} + +/* ── Batch bar ── */ +.watchlist-page-container .watchlist-batch-bar { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + padding: 8px 16px; + margin-bottom: 16px; +} + +/* ── Override banner ── */ +.watchlist-page-container .watchlist-global-override-banner { + background: linear-gradient(135deg, + rgba(255, 193, 7, 0.08) 0%, + rgba(255, 152, 0, 0.05) 100%); + border: 1px solid rgba(255, 193, 7, 0.15); + border-radius: 10px; + padding: 10px 16px; + margin-bottom: 16px; + font-size: 13px; + color: rgba(255, 193, 7, 0.85); + display: flex; + align-items: center; + gap: 8px; +} + +/* ── Empty state ── */ +.watchlist-page-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px 20px; + text-align: center; +} + +.watchlist-page-empty-icon { + opacity: 0.15; + margin-bottom: 8px; +} + +.watchlist-page-empty h3 { + margin: 12px 0 8px; + color: rgba(255, 255, 255, 0.7); + font-size: 20px; + font-weight: 700; + letter-spacing: -0.3px; +} + +.watchlist-page-empty p { + margin: 0 0 24px; + font-size: 14px; + color: rgba(255, 255, 255, 0.35); + max-width: 360px; + line-height: 1.5; +} + +/* ═══════════════════════════════════════════════════════════════════ + WISHLIST PAGE + ═══════════════════════════════════════════════════════════════════ */ + +.wishlist-page-container { + padding: 28px 24px 30px; + max-width: 1400px; + margin: 0 auto; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.55) 0%, + rgba(12, 12, 12, 0.62) 100%); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + position: relative; +} + +/* ── Header ── */ +.wishlist-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 20px 24px 18px; + margin: -28px -24px 24px -24px; + background: linear-gradient(180deg, + rgba(var(--accent-rgb), 0.08) 0%, + rgba(var(--accent-rgb), 0.03) 40%, + transparent 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-top-left-radius: 24px; + border-top-right-radius: 24px; + position: relative; +} + +.wishlist-page-header::after { + content: ''; + position: absolute; + bottom: -1px; + left: 10%; + right: 10%; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + rgba(var(--accent-rgb), 0.2) 20%, + rgba(var(--accent-rgb), 0.35) 50%, + rgba(var(--accent-rgb), 0.2) 80%, + transparent 100%); +} + +.wishlist-page-header-left { + display: flex; + flex-direction: column; + gap: 6px; +} + +.wishlist-page-header-right { + display: flex; + gap: 8px; + align-items: center; +} + +.wishlist-page-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 26px; + font-weight: 700; + color: #fff; + margin: 0; + letter-spacing: -0.5px; + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.wishlist-page-title-icon { + font-size: 26px; +} + +.wishlist-page-meta { + display: flex; + align-items: center; + gap: 16px; + font-size: 13px; + color: rgba(255, 255, 255, 0.45); + font-weight: 500; +} + +.wishlist-page-count { + color: rgb(var(--accent-light-rgb)); + font-weight: 600; +} + +.wishlist-page-timer { + padding-left: 16px; + border-left: 1px solid rgba(255, 255, 255, 0.08); +} + +/* ── Stats strip ── */ +.wishlist-stats-strip { + display: flex; + align-items: center; + justify-content: center; + gap: 0; + padding: 14px 24px; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.95) 0%, + rgba(12, 12, 12, 0.98) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + margin-bottom: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.wishlist-stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 0 32px; +} + +.wishlist-stat-value { + font-size: 22px; + font-weight: 700; + color: #fff; + letter-spacing: -0.5px; + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.wishlist-stat-value.wishlist-stat-cycle { + font-size: 14px; + font-weight: 600; + color: rgb(var(--accent-light-rgb)); +} + +.wishlist-stat-label { + font-size: 11px; + font-weight: 500; + color: rgba(255, 255, 255, 0.35); + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.wishlist-stat-divider { + width: 1px; + height: 32px; + background: rgba(255, 255, 255, 0.08); +} + +/* ── Track search ── */ +.wishlist-track-search-container { + position: relative; + margin-bottom: 12px; +} + +.wishlist-track-search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} + +.wishlist-track-search-input { + width: 100%; + padding: 9px 16px 9px 36px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + background: rgba(255, 255, 255, 0.03); + color: #fff; + font-size: 13px; + font-weight: 500; + transition: all 0.2s; + box-sizing: border-box; +} + +.wishlist-track-search-input:focus { + border-color: rgba(var(--accent-rgb), 0.4); + background: rgba(255, 255, 255, 0.05); + box-shadow: 0 0 10px rgba(var(--accent-rgb), 0.1); + outline: none; +} + +.wishlist-track-search-input::placeholder { + color: rgba(255, 255, 255, 0.3); +} + +/* ── Category cards upgrade ── */ +#wishlist-page-categories { + margin-bottom: 24px; +} + +.wishlist-page-container .wishlist-category-card { + border: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 6px 20px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.wishlist-page-container .wishlist-category-card:hover { + transform: translateY(-4px) scale(1.01); + border-color: rgba(var(--accent-rgb), 0.2); + box-shadow: + 0 12px 30px rgba(0, 0, 0, 0.4), + 0 0 24px rgba(var(--accent-rgb), 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.wishlist-page-container .wishlist-category-card.next-in-queue { + border-color: rgba(var(--accent-rgb), 0.25); + box-shadow: + 0 6px 20px rgba(0, 0, 0, 0.35), + 0 0 16px rgba(var(--accent-rgb), 0.06), + inset 0 1px 0 rgba(var(--accent-rgb), 0.1); +} + +/* ── Mosaic background ── */ +.wishlist-mosaic-bg { + position: absolute; + inset: 0; + overflow: hidden; + border-radius: inherit; + z-index: 0; +} + +.wishlist-mosaic-bg::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.55)); + z-index: 1; +} + +/* ── Track list section ── */ +.wishlist-page-container .wishlist-category-tracks { + max-height: none; + background: rgba(255, 255, 255, 0.015); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 14px; + padding: 16px; +} + +.wishlist-page-container .wishlist-category-header { + margin-bottom: 12px; +} + +.wishlist-page-container .wishlist-tracks-scroll { + max-height: calc(100vh - 320px); + overflow-y: auto; +} + +/* Category header right side with download button */ +.wishlist-category-header-right { + display: flex; + align-items: center; + gap: 8px; +} + +/* ── Batch bar ── */ +.wishlist-page-container .wishlist-batch-bar { + background: rgba(var(--accent-rgb), 0.06); + border: 1px solid rgba(var(--accent-rgb), 0.12); + border-radius: 10px; + padding: 8px 16px; + margin-bottom: 12px; +} + +/* ── Album cards in track list ── */ +.wishlist-page-container .wishlist-album-card { + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 12px; + background: rgba(255, 255, 255, 0.02); + transition: border-color 0.2s; +} + +.wishlist-page-container .wishlist-album-card:hover { + border-color: rgba(255, 255, 255, 0.1); +} + +/* ── Empty state ── */ +.wishlist-page-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px 20px; + text-align: center; +} + +.wishlist-page-empty-icon { + opacity: 0.15; + margin-bottom: 8px; +} + +.wishlist-page-empty h3 { + margin: 12px 0 8px; + color: rgba(255, 255, 255, 0.7); + font-size: 20px; + font-weight: 700; + letter-spacing: -0.3px; +} + +.wishlist-page-empty p { + margin: 0 0 24px; + font-size: 14px; + color: rgba(255, 255, 255, 0.35); + max-width: 360px; + line-height: 1.5; +} + +/* ═══════════════════════════════════════════════════════════════════ + WATCHLIST & WISHLIST RESPONSIVE + ═══════════════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + .watchlist-page-container, + .wishlist-page-container { + padding: 16px; + border-radius: 16px; + } + + .watchlist-page-header, + .wishlist-page-header { + flex-direction: column; + gap: 12px; + padding: 16px; + margin: -16px -16px 16px -16px; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + } + + .watchlist-page-title, + .wishlist-page-title { + font-size: 22px; + } + + .watchlist-page-actions { + flex-direction: column; + align-items: stretch; + } + + .watchlist-page-container .watchlist-artists-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; + } + + .watchlist-toolbar { + flex-direction: column; + } + + .watchlist-sort-select { + width: 100%; + } + + .wishlist-page-header-right { + width: 100%; + justify-content: flex-end; + } + + .wishlist-stat-item { + padding: 0 16px; + } + + .wishlist-stat-value { + font-size: 18px; + } +}
Add artists from the Artists page to monitor them for new releases.
Failed downloads and tracks from watchlist scans will appear here automatically.