Promote Watchlist and Wishlist from modals to full sidebar pages
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.
This commit is contained in:
parent
316d4cb466
commit
cf18590794
4 changed files with 1472 additions and 41 deletions
233
webui/index.html
233
webui/index.html
|
|
@ -212,6 +212,16 @@
|
|||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg></span>
|
||||
<span class="nav-text">Artists</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="watchlist">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span>
|
||||
<span class="nav-text">Watchlist</span>
|
||||
<span class="dl-nav-badge hidden" id="watchlist-nav-badge">0</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="wishlist">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/><line x1="1" y1="1" x2="5" y2="5" stroke-width="0"/></svg></span>
|
||||
<span class="nav-text">Wishlist</span>
|
||||
<span class="dl-nav-badge hidden" id="wishlist-nav-badge">0</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="automations">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>
|
||||
<span class="nav-text">Automations</span>
|
||||
|
|
@ -6401,6 +6411,229 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════
|
||||
WATCHLIST PAGE
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<div class="page" id="watchlist-page">
|
||||
<div class="watchlist-page-container">
|
||||
<!-- Header -->
|
||||
<div class="watchlist-page-header">
|
||||
<div class="watchlist-page-header-left">
|
||||
<h2 class="watchlist-page-title">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="rgb(var(--accent-rgb))"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
Watchlist
|
||||
</h2>
|
||||
<div class="watchlist-page-meta">
|
||||
<span class="watchlist-page-count" id="watchlist-page-count">0 artists</span>
|
||||
<span class="watchlist-page-timer" id="watchlist-next-auto-timer">Next Auto: --</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scan status / live activity (IDs match old modal for compatibility) -->
|
||||
<div id="watchlist-scan-status" class="watchlist-page-scan-status" style="display: none;">
|
||||
<div id="watchlist-live-activity" class="watchlist-live-activity" style="display: none;">
|
||||
<div class="watchlist-live-activity-col">
|
||||
<img id="watchlist-artist-img" class="watchlist-live-activity-artist-img" src="" alt="Artist" onerror="this.style.display='none';" />
|
||||
<div id="watchlist-artist-name" class="watchlist-live-activity-label">Waiting...</div>
|
||||
</div>
|
||||
<div class="watchlist-live-activity-col">
|
||||
<img id="watchlist-album-img" class="watchlist-live-activity-album-img" src="" alt="Album" onerror="this.style.display='none';" />
|
||||
<div id="watchlist-album-name" class="watchlist-live-activity-label">Waiting...</div>
|
||||
</div>
|
||||
<div class="watchlist-live-activity-feed">
|
||||
<div class="watchlist-live-activity-feed-label">Current Track:</div>
|
||||
<div id="watchlist-track-name" class="watchlist-live-activity-track">Waiting...</div>
|
||||
<div class="watchlist-live-activity-feed-label-orange">Recently Added:</div>
|
||||
<div id="watchlist-additions-feed" style="max-height: 80px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; font-size: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watchlist-page-scan-summary" class="scan-status-summary" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="watchlist-page-actions">
|
||||
<button class="watchlist-action-btn watchlist-action-primary" id="scan-watchlist-btn" onclick="startWatchlistScan()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
Scan for New Releases
|
||||
</button>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary" id="cancel-watchlist-scan-btn" onclick="cancelWatchlistScan()" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
Cancel Scan
|
||||
</button>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary" id="update-similar-artists-btn" onclick="updateSimilarArtists()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
Update Similar Artists
|
||||
</button>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary" id="watchlist-page-settings-btn" onclick="openWatchlistGlobalSettingsModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
Global Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Global override banner -->
|
||||
<div class="watchlist-global-override-banner" id="watchlist-page-override-banner" style="display: none;">
|
||||
<span>⚠️</span>
|
||||
<span>Global override is active — per-artist settings are being ignored during scans.</span>
|
||||
</div>
|
||||
|
||||
<!-- Last scan summary -->
|
||||
<div class="watchlist-last-scan-strip" id="watchlist-last-scan-strip" style="display: none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span id="watchlist-last-scan-text">Last scan: --</span>
|
||||
</div>
|
||||
|
||||
<!-- Search + Sort row -->
|
||||
<div class="watchlist-toolbar">
|
||||
<div class="watchlist-search-container">
|
||||
<svg class="watchlist-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="rgba(255,255,255,0.35)"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<input type="text" id="watchlist-search-input" class="watchlist-search-input" placeholder="Search watchlist..." oninput="filterWatchlistArtists()">
|
||||
</div>
|
||||
<select class="watchlist-sort-select" id="watchlist-sort-select" onchange="sortWatchlistArtists(this.value)">
|
||||
<option value="name-asc">Name A-Z</option>
|
||||
<option value="name-desc">Name Z-A</option>
|
||||
<option value="scan-oldest">Oldest Scanned</option>
|
||||
<option value="scan-newest">Recently Scanned</option>
|
||||
<option value="added-newest">Recently Added</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Batch bar -->
|
||||
<div class="watchlist-batch-bar" id="watchlist-batch-bar">
|
||||
<label class="watchlist-select-all-label" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="watchlist-select-all-cb" onchange="toggleWatchlistSelectAll(this.checked)">
|
||||
<span>Select All</span>
|
||||
</label>
|
||||
<span class="watchlist-batch-count" id="watchlist-batch-count"></span>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary watchlist-batch-remove-btn" id="watchlist-batch-remove-btn" onclick="batchRemoveFromWatchlist()" style="display: none;">
|
||||
Remove Selected
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Artist grid -->
|
||||
<div class="watchlist-artists-grid" id="watchlist-artists-list">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="watchlist-page-empty" id="watchlist-page-empty" style="display: none;">
|
||||
<div class="watchlist-page-empty-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</div>
|
||||
<h3>Your watchlist is empty</h3>
|
||||
<p>Add artists from the Artists page to monitor them for new releases.</p>
|
||||
<button class="watchlist-action-btn watchlist-action-primary" onclick="navigateToPage('artists')">Browse Artists</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════
|
||||
WISHLIST PAGE
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<div class="page" id="wishlist-page">
|
||||
<div class="wishlist-page-container">
|
||||
<!-- Header -->
|
||||
<div class="wishlist-page-header">
|
||||
<div class="wishlist-page-header-left">
|
||||
<h2 class="wishlist-page-title">
|
||||
<span class="wishlist-page-title-icon">🎵</span>
|
||||
Wishlist
|
||||
</h2>
|
||||
<div class="wishlist-page-meta">
|
||||
<span class="wishlist-page-count" id="wishlist-page-count">0 tracks</span>
|
||||
<span class="wishlist-page-timer" id="wishlist-next-auto-timer">Next Auto: --</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wishlist-page-header-right">
|
||||
<button class="watchlist-action-btn watchlist-action-secondary" onclick="cleanupWishlistOverview()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/></svg>
|
||||
Cleanup
|
||||
</button>
|
||||
<button class="watchlist-action-btn watchlist-action-danger" onclick="clearEntireWishlist()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats strip -->
|
||||
<div class="wishlist-stats-strip" id="wishlist-stats-strip">
|
||||
<div class="wishlist-stat-item">
|
||||
<span class="wishlist-stat-value" id="wishlist-stat-albums">0</span>
|
||||
<span class="wishlist-stat-label">Album Tracks</span>
|
||||
</div>
|
||||
<div class="wishlist-stat-divider"></div>
|
||||
<div class="wishlist-stat-item">
|
||||
<span class="wishlist-stat-value" id="wishlist-stat-singles">0</span>
|
||||
<span class="wishlist-stat-label">Singles</span>
|
||||
</div>
|
||||
<div class="wishlist-stat-divider"></div>
|
||||
<div class="wishlist-stat-item">
|
||||
<span class="wishlist-stat-value wishlist-stat-cycle" id="wishlist-stat-cycle">Albums/EPs</span>
|
||||
<span class="wishlist-stat-label">Next Cycle</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category grid -->
|
||||
<div class="wishlist-category-grid" id="wishlist-page-categories">
|
||||
<div class="wishlist-category-card" data-category="albums" onclick="selectWishlistCategory('albums')">
|
||||
<div class="wishlist-mosaic-bg" id="wishlist-page-albums-mosaic"></div>
|
||||
<div class="wishlist-category-content">
|
||||
<div class="wishlist-category-icon">💿</div>
|
||||
<div class="wishlist-category-title">Albums / EPs</div>
|
||||
<div class="wishlist-category-count" id="wishlist-page-albums-count">0 tracks</div>
|
||||
<div class="wishlist-category-badge" id="wishlist-page-albums-badge" style="display: none;">Next in Queue</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wishlist-category-card" data-category="singles" onclick="selectWishlistCategory('singles')">
|
||||
<div class="wishlist-mosaic-bg" id="wishlist-page-singles-mosaic"></div>
|
||||
<div class="wishlist-category-content">
|
||||
<div class="wishlist-category-icon">🎵</div>
|
||||
<div class="wishlist-category-title">Singles</div>
|
||||
<div class="wishlist-category-count" id="wishlist-page-singles-count">0 tracks</div>
|
||||
<div class="wishlist-category-badge" id="wishlist-page-singles-badge" style="display: none;">Next in Queue</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Track list (hidden until category selected) -->
|
||||
<div id="wishlist-category-tracks" class="wishlist-category-tracks" style="display: none;">
|
||||
<div class="wishlist-category-header">
|
||||
<button class="wishlist-back-btn" onclick="backToCategories()">← Back</button>
|
||||
<span id="wishlist-category-name" class="wishlist-category-name"></span>
|
||||
<div class="wishlist-category-header-right">
|
||||
<button class="wishlist-select-all-btn" id="wishlist-select-all-btn" onclick="toggleWishlistSelectAll()">Select All</button>
|
||||
<button id="wishlist-download-btn" class="watchlist-action-btn watchlist-action-primary" style="display: none;" onclick="downloadSelectedCategory()">
|
||||
Download Selection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wishlist-track-search-container">
|
||||
<svg class="wishlist-track-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="rgba(255,255,255,0.35)"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<input type="text" id="wishlist-track-search-input" class="wishlist-track-search-input" placeholder="Filter tracks..." oninput="filterWishlistTracks()">
|
||||
</div>
|
||||
<div class="wishlist-batch-bar" id="wishlist-batch-bar" style="display: none;">
|
||||
<span class="wishlist-batch-count" id="wishlist-batch-count">0 selected</span>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary wishlist-batch-remove-btn" onclick="batchRemoveFromWishlist()">
|
||||
Remove Selected
|
||||
</button>
|
||||
</div>
|
||||
<div id="wishlist-tracks-list" class="wishlist-tracks-scroll">
|
||||
<div class="loading-indicator">Loading tracks...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="wishlist-page-empty" id="wishlist-page-empty" style="display: none;">
|
||||
<div class="wishlist-page-empty-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
||||
</div>
|
||||
<h3>Your wishlist is empty</h3>
|
||||
<p>Failed downloads and tracks from watchlist scans will appear here automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /main-content -->
|
||||
</div> <!-- /main-container -->
|
||||
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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 = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg> ${globalOverrideActive ? 'Global Override ON' : 'Global Settings'}`;
|
||||
}
|
||||
|
||||
// Render artist cards
|
||||
if (gridEl) {
|
||||
gridEl.innerHTML = artists.map(artist => {
|
||||
const pills = [];
|
||||
if (artist.include_albums) pills.push('<span class="watchlist-pill watchlist-pill-active">Albums</span>');
|
||||
if (artist.include_eps) pills.push('<span class="watchlist-pill watchlist-pill-active">EPs</span>');
|
||||
if (artist.include_singles) pills.push('<span class="watchlist-pill watchlist-pill-active">Singles</span>');
|
||||
if (artist.include_live) pills.push('<span class="watchlist-pill watchlist-pill-filter">Live</span>');
|
||||
if (artist.include_remixes) pills.push('<span class="watchlist-pill watchlist-pill-filter">Remixes</span>');
|
||||
if (artist.include_acoustic) pills.push('<span class="watchlist-pill watchlist-pill-filter">Acoustic</span>');
|
||||
if (artist.include_compilations) pills.push('<span class="watchlist-pill watchlist-pill-filter">Compilations</span>');
|
||||
const sourceBadges = [];
|
||||
if (artist.spotify_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-spotify">Spotify</span>');
|
||||
if (artist.itunes_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-itunes">iTunes</span>');
|
||||
if (artist.deezer_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-deezer">Deezer</span>');
|
||||
if (artist.discogs_artist_id) sourceBadges.push('<span class="watchlist-source-badge watchlist-source-discogs">Discogs</span>');
|
||||
const artistPrimaryId = artist.spotify_artist_id || artist.itunes_artist_id || artist.deezer_artist_id || artist.discogs_artist_id;
|
||||
return `
|
||||
<div class="watchlist-artist-card"
|
||||
data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '"')}"
|
||||
data-artist-id="${artistPrimaryId}"
|
||||
data-last-scan="${artist.last_scan_timestamp || ''}"
|
||||
data-added="${artist.date_added || ''}">
|
||||
<label class="watchlist-card-checkbox" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" class="watchlist-select-cb"
|
||||
data-artist-id="${artistPrimaryId}"
|
||||
data-artist-name="${escapeHtml(artist.artist_name)}"
|
||||
onchange="updateWatchlistBatchBar()">
|
||||
<span class="watchlist-checkbox-custom"></span>
|
||||
</label>
|
||||
<button class="watchlist-card-gear"
|
||||
data-artist-id="${artistPrimaryId}"
|
||||
data-artist-name="${escapeHtml(artist.artist_name)}"
|
||||
onclick="event.stopPropagation();"
|
||||
title="Artist settings">
|
||||
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.48.48 0 00-.48-.41h-3.84a.48.48 0 00-.48.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87a.48.48 0 00.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.26.41.48.41h3.84c.24 0 .44-.17.48-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1115.6 12 3.6 3.6 0 0112 15.6z"/></svg>
|
||||
</button>
|
||||
<div class="watchlist-card-image">
|
||||
${artist.image_url ? `<img src="${artist.image_url}" alt="${escapeHtml(artist.artist_name)}" onerror="if(!this.dataset.retried){this.dataset.retried='1';this.src=this.src;}else{this.parentElement.innerHTML='<div class=\\'watchlist-card-image-fallback\\'>🎤</div>';}">` : '<div class="watchlist-card-image-fallback">🎤</div>'}
|
||||
</div>
|
||||
<div class="watchlist-card-info">
|
||||
<span class="watchlist-card-name">${escapeHtml(artist.artist_name)}</span>
|
||||
<span class="watchlist-card-meta">${formatRelativeScanTime(artist.last_scan_timestamp)}</span>
|
||||
</div>
|
||||
${sourceBadges.length > 0 ? `<div class="watchlist-card-sources">${sourceBadges.join('')}</div>` : ''}
|
||||
${pills.length > 0 ? `<div class="watchlist-card-pills">${pills.join('')}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> 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 = `<span class="sync-stat">Artists: ${statusRes.summary.total_artists || 0}</span><span class="sync-separator"> • </span><span class="sync-stat">New tracks: ${statusRes.summary.new_tracks_found || 0}</span><span class="sync-separator"> • </span><span class="sync-stat">Added to wishlist: ${statusRes.summary.tracks_added_to_wishlist || 0}</span>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue