From 09d633f80b8df788d4655d02b9cb0f8085967c43 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Wed, 24 Sep 2025 22:30:38 -0700 Subject: [PATCH] add to wishlist functionality for library page --- web_server.py | 79 ++++++ webui/index.html | 42 +++- webui/static/script.js | 494 ++++++++++++++++++++++++++++++++++++- webui/static/style.css | 538 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1139 insertions(+), 14 deletions(-) diff --git a/web_server.py b/web_server.py index 74f2aabf..cda4fb23 100644 --- a/web_server.py +++ b/web_server.py @@ -6584,6 +6584,85 @@ def cleanup_wishlist(): traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/add-album-to-wishlist', methods=['POST']) +def add_album_track_to_wishlist(): + """Endpoint to add a single track from an album to the wishlist.""" + try: + from core.wishlist_service import get_wishlist_service + + data = request.get_json() + if not data: + return jsonify({"success": False, "error": "No data provided"}), 400 + + track = data.get('track') + artist = data.get('artist') + album = data.get('album') + source_type = data.get('source_type', 'album') + source_context = data.get('source_context', {}) + + if not track or not artist or not album: + return jsonify({"success": False, "error": "Missing required fields: track, artist, album"}), 400 + + # Create Spotify track data format expected by wishlist service + spotify_track_data = { + 'id': track.get('id'), + 'name': track.get('name'), + 'artists': track.get('artists', []), + 'album': { + 'id': album.get('id'), + 'name': album.get('name'), + 'images': album.get('images', []) if 'images' in album else [{'url': album.get('image_url', '')}], + 'album_type': album.get('album_type', 'album'), + 'release_date': album.get('release_date', ''), + 'total_tracks': album.get('total_tracks', 1) + }, + 'duration_ms': track.get('duration_ms', 0), + 'track_number': track.get('track_number', 1), + 'explicit': track.get('explicit', False), + 'popularity': track.get('popularity', 0), + 'preview_url': track.get('preview_url'), + 'external_urls': track.get('external_urls', {}) + } + + # Add source context information + enhanced_source_context = { + **source_context, + 'artist_id': artist.get('id'), + 'artist_name': artist.get('name'), + 'album_id': album.get('id'), + 'album_name': album.get('name'), + 'added_via': 'library_wishlist_modal' + } + + # Get wishlist service and add track + wishlist_service = get_wishlist_service() + + success = wishlist_service.add_spotify_track_to_wishlist( + spotify_track_data=spotify_track_data, + failure_reason="Added from library (incomplete album)", + source_type=source_type, + source_context=enhanced_source_context + ) + + if success: + print(f"✅ Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist") + return jsonify({ + "success": True, + "message": f"Added '{track.get('name')}' to wishlist" + }) + else: + print(f"❌ Failed to add track '{track.get('name')}' to wishlist") + return jsonify({ + "success": False, + "error": "Failed to add track to wishlist" + }) + + except Exception as e: + print(f"❌ Error adding track to wishlist: {e}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/api/database/update', methods=['POST']) def start_database_update(): """Endpoint to start the database update process.""" diff --git a/webui/index.html b/webui/index.html index faafefec..86be8508 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1241,7 +1241,47 @@ - + + + + \ No newline at end of file diff --git a/webui/static/script.js b/webui/static/script.js index d8a88f23..b1bc6973 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -6297,6 +6297,300 @@ function getSuccessfulDownloadCount(process) { } } +// =============================== +// ADD TO WISHLIST MODAL FUNCTIONS +// =============================== + +let currentWishlistModalData = null; + +/** + * Open the Add to Wishlist modal for an album/EP/single + * @param {Object} album - Album object with id, name, image_url, etc. + * @param {Object} artist - Artist object with id, name, image_url + * @param {Array} tracks - Array of track objects + * @param {string} albumType - Type of release (album, EP, single) + */ +async function openAddToWishlistModal(album, artist, tracks, albumType) { + console.log(`🎵 Opening Add to Wishlist modal for: ${artist.name} - ${album.name}`); + + try { + // Store current modal data for use by other functions + currentWishlistModalData = { + album, + artist, + tracks, + albumType + }; + + const modal = document.getElementById('add-to-wishlist-modal'); + const overlay = document.getElementById('add-to-wishlist-modal-overlay'); + + if (!modal || !overlay) { + console.error('Add to wishlist modal elements not found'); + return; + } + + // Generate and populate hero section + const heroContent = generateWishlistModalHeroSection(album, artist, tracks, albumType); + const heroContainer = document.getElementById('add-to-wishlist-modal-hero'); + if (heroContainer) { + heroContainer.innerHTML = heroContent; + } + + // Generate and populate track list + const trackListHTML = generateWishlistTrackList(tracks); + const trackListContainer = document.getElementById('wishlist-track-list'); + if (trackListContainer) { + trackListContainer.innerHTML = trackListHTML; + } + + // Set up the "Add to Wishlist" button click handler + const addToWishlistBtn = document.getElementById('confirm-add-to-wishlist-btn'); + if (addToWishlistBtn) { + addToWishlistBtn.onclick = () => handleAddToWishlist(); + } + + // Show the modal + overlay.classList.remove('hidden'); + + console.log(`✅ Successfully opened Add to Wishlist modal for: ${album.name}`); + + } catch (error) { + console.error('❌ Error opening Add to Wishlist modal:', error); + showToast(`Error opening wishlist modal: ${error.message}`, 'error'); + } +} + +/** + * Generate the hero section HTML for the wishlist modal + */ +function generateWishlistModalHeroSection(album, artist, tracks, albumType) { + const artistImage = artist.image_url || ''; + const albumImage = album.image_url || ''; + const trackCount = tracks.length; + + let heroBackgroundImage = ''; + if (albumImage) { + heroBackgroundImage = `
`; + } + + const heroContent = ` +
+
+ ${artistImage ? `${escapeHtml(artist.name)}` : ''} + ${albumImage ? `${escapeHtml(album.name)}` : ''} +
+ +
+ `; + + return ` + ${heroBackgroundImage} + ${heroContent} + `; +} + +/** + * Generate the track list HTML for the wishlist modal + */ +function generateWishlistTrackList(tracks) { + if (!tracks || tracks.length === 0) { + return '
No tracks found
'; + } + + return tracks.map((track, index) => { + const trackNumber = track.track_number || (index + 1); + const trackName = escapeHtml(track.name || 'Unknown Track'); + const artistsString = formatArtists(track.artists) || 'Unknown Artist'; + const duration = formatDuration(track.duration_ms); + + return ` +
+
${trackNumber}
+
+
${trackName}
+
${artistsString}
+
+
${duration}
+
+ `; + }).join(''); +} + +/** + * Handle the "Add to Wishlist" button click + */ +async function handleAddToWishlist() { + if (!currentWishlistModalData) { + console.error('❌ No wishlist modal data available'); + return; + } + + const { album, artist, tracks, albumType } = currentWishlistModalData; + const addToWishlistBtn = document.getElementById('confirm-add-to-wishlist-btn'); + + try { + // Show loading state + if (addToWishlistBtn) { + addToWishlistBtn.classList.add('loading'); + addToWishlistBtn.textContent = 'Adding...'; + addToWishlistBtn.disabled = true; + } + + console.log(`🔄 Adding ${tracks.length} tracks to wishlist for: ${artist.name} - ${album.name}`); + + let successCount = 0; + let errorCount = 0; + + // Add each track to wishlist individually + for (const track of tracks) { + try { + // Ensure artists field is in the correct format (array of objects) + let formattedArtists = track.artists; + if (typeof track.artists === 'string') { + // If artists is a string, convert to array of objects + formattedArtists = [{ name: track.artists }]; + } else if (Array.isArray(track.artists)) { + // If artists is already an array, ensure each item is an object + formattedArtists = track.artists.map(artistItem => { + if (typeof artistItem === 'string') { + return { name: artistItem }; + } else if (typeof artistItem === 'object' && artistItem !== null) { + return artistItem; + } else { + return { name: 'Unknown Artist' }; + } + }); + } else { + // Fallback to array with single artist object + formattedArtists = [{ name: artist.name }]; + } + + const formattedTrack = { + ...track, + artists: formattedArtists + }; + + console.log(`🔄 Adding track with formatted artists:`, formattedTrack.name, formattedTrack.artists); + + const response = await fetch('/api/add-album-to-wishlist', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + track: formattedTrack, + artist: artist, + album: album, + source_type: 'album', + source_context: { + album_name: album.name, + artist_name: artist.name, + album_type: albumType + } + }) + }); + + const result = await response.json(); + + if (result.success) { + successCount++; + console.log(`✅ Added "${track.name}" to wishlist`); + } else { + errorCount++; + console.error(`❌ Failed to add "${track.name}" to wishlist: ${result.error}`); + } + + } catch (error) { + errorCount++; + console.error(`❌ Error adding "${track.name}" to wishlist:`, error); + } + } + + // Show completion message + if (successCount > 0) { + const message = errorCount > 0 + ? `Added ${successCount}/${tracks.length} tracks to wishlist (${errorCount} failed)` + : `Added ${successCount} tracks to wishlist`; + showToast(message, successCount === tracks.length ? 'success' : 'warning'); + } else { + showToast('Failed to add any tracks to wishlist', 'error'); + } + + // Close the modal + closeAddToWishlistModal(); + + console.log(`✅ Wishlist addition complete: ${successCount} successful, ${errorCount} failed`); + + } catch (error) { + console.error('❌ Error in handleAddToWishlist:', error); + showToast(`Error adding to wishlist: ${error.message}`, 'error'); + } finally { + // Reset button state + if (addToWishlistBtn) { + addToWishlistBtn.classList.remove('loading'); + addToWishlistBtn.textContent = 'Add to Wishlist'; + addToWishlistBtn.disabled = false; + } + } +} + +/** + * Close the Add to Wishlist modal + */ +function closeAddToWishlistModal() { + console.log('🔄 Closing Add to Wishlist modal'); + + try { + const overlay = document.getElementById('add-to-wishlist-modal-overlay'); + if (overlay) { + overlay.classList.add('hidden'); + } + + // Clear current modal data + currentWishlistModalData = null; + + // Clear hero content + const heroContainer = document.getElementById('add-to-wishlist-modal-hero'); + if (heroContainer) { + heroContainer.innerHTML = ''; + } + + // Clear track list + const trackListContainer = document.getElementById('wishlist-track-list'); + if (trackListContainer) { + trackListContainer.innerHTML = ''; + } + + console.log('✅ Add to Wishlist modal closed successfully'); + + } catch (error) { + console.error('❌ Error closing Add to Wishlist modal:', error); + } +} + +/** + * Format duration from milliseconds to MM:SS format + */ +function formatDuration(durationMs) { + if (!durationMs || durationMs <= 0) { + return '--:--'; + } + + const totalSeconds = Math.floor(durationMs / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} + // Download Missing Tracks Modal functions window.openDownloadMissingModal = openDownloadMissingModal; window.closeDownloadMissingModal = closeDownloadMissingModal; @@ -6306,11 +6600,16 @@ window.cancelTrackDownload = cancelTrackDownload; // Legacy system window.cancelTrackDownloadV2 = cancelTrackDownloadV2; // NEW V2 system window.handleViewProgressClick = handleViewProgressClick; -// Wishlist Modal functions +// Wishlist Modal functions (existing) window.openDownloadMissingWishlistModal = openDownloadMissingWishlistModal; window.startWishlistMissingTracksProcess = startWishlistMissingTracksProcess; window.handleWishlistButtonClick = handleWishlistButtonClick; +// Add to Wishlist Modal functions (new) +window.openAddToWishlistModal = openAddToWishlistModal; +window.closeAddToWishlistModal = closeAddToWishlistModal; +window.handleAddToWishlist = handleAddToWishlist; + // Helper functions window.escapeHtml = escapeHtml; window.formatArtists = formatArtists; @@ -11536,22 +11835,109 @@ function showSearchLoadingCards() { // ARTIST ALBUM DOWNLOAD MISSING TRACKS INTEGRATION // =============================== +/** + * Get the completion status of an album from cached data or DOM + * @param {string} albumId - The album ID + * @param {string} albumType - The album type ('albums' or 'singles') + * @returns {Object|null} - Completion status object or null + */ +function getAlbumCompletionStatus(albumId, albumType) { + try { + // First, check cached completion data + const artistId = artistsPageState.selectedArtist?.id; + if (artistId && artistsPageState.cache.completionData[artistId]) { + const cachedData = artistsPageState.cache.completionData[artistId]; + const dataArray = albumType === 'albums' ? cachedData.albums : cachedData.singles; + + if (dataArray) { + const completionData = dataArray.find(item => item.album_id === albumId || item.id === albumId); + if (completionData) { + console.log(`📊 Found cached completion data for album ${albumId}:`, completionData); + return completionData; + } + } + } + + // Fallback: Check DOM completion overlay + const containerId = albumType === 'albums' ? 'album-cards-container' : 'singles-cards-container'; + const container = document.getElementById(containerId); + + if (container) { + const albumCard = container.querySelector(`[data-album-id="${albumId}"]`); + if (albumCard) { + const overlay = albumCard.querySelector('.completion-overlay'); + if (overlay) { + // Extract status from overlay classes + const classList = Array.from(overlay.classList); + const statusClasses = ['completed', 'nearly_complete', 'partial', 'missing', 'downloading', 'downloaded', 'error']; + const status = statusClasses.find(cls => classList.includes(cls)); + + if (status) { + console.log(`📊 Found DOM completion status for album ${albumId}: ${status}`); + return { status, completion_percentage: status === 'completed' ? 100 : 0 }; + } + } + } + } + + console.warn(`⚠️ No completion status found for album ${albumId}`); + return null; + + } catch (error) { + console.error(`❌ Error getting album completion status for ${albumId}:`, error); + return null; + } +} + /** * Handle album/single/EP click to open download missing tracks modal */ async function handleArtistAlbumClick(album, albumType) { console.log(`🎵 Album clicked: ${album.name} (${album.album_type}) from artist: ${artistsPageState.selectedArtist?.name}`); - + if (!artistsPageState.selectedArtist) { console.error('❌ No selected artist found'); showToast('Error: No artist selected', 'error'); return; } - + try { + // Check completion status of the album + const completionStatus = getAlbumCompletionStatus(album.id, albumType); + console.log(`📊 Album completion status: ${completionStatus?.status || 'unknown'} (${completionStatus?.completion_percentage || 0}%)`); + + // If album is complete, show informational message and exit + if (completionStatus?.status === 'completed') { + showToast(`${album.name} is already complete in your library`, 'info'); + return; + } + + // If album has missing tracks, show Add to Wishlist modal + if (completionStatus?.status && ['partial', 'missing', 'nearly_complete'].includes(completionStatus.status)) { + console.log(`🎵 Album has missing tracks, opening Add to Wishlist modal`); + + // Load tracks for the album + const response = await fetch(`/api/artist/${artistsPageState.selectedArtist.id}/album/${album.id}/tracks`); + if (!response.ok) { + throw new Error(`Failed to load album tracks: ${response.status}`); + } + + const data = await response.json(); + if (!data.success || !data.tracks || data.tracks.length === 0) { + throw new Error('No tracks found for this album'); + } + + // Open the Add to Wishlist modal + await openAddToWishlistModal(album, artistsPageState.selectedArtist, data.tracks, albumType); + return; + } + + // For unknown status or if we want to check what's missing, use the existing download modal + console.log(`🔄 Opening existing download missing tracks modal for status analysis`); + // Create virtual playlist ID const virtualPlaylistId = `artist_album_${artistsPageState.selectedArtist.id}_${album.id}`; - + // Check if modal already exists and show it if (activeDownloadProcesses[virtualPlaylistId]) { console.log(`📱 Reopening existing modal for ${album.name}`); @@ -11564,12 +11950,12 @@ async function handleArtistAlbumClick(album, albumType) { return; } } - + // Create virtual playlist and open modal await createArtistAlbumVirtualPlaylist(album, albumType); - + } catch (error) { - console.error('❌ Error opening download missing tracks modal:', error); + console.error('❌ Error handling album click:', error); showToast(`Error opening download modal: ${error.message}`, 'error'); } } @@ -14373,19 +14759,101 @@ function createReleaseCard(release) { card.appendChild(year); card.appendChild(completion); - // Add click handler for future functionality - card.addEventListener("click", () => { + // Add click handler for release card + card.addEventListener("click", async () => { console.log(`Clicked on release: ${release.title} (Owned: ${release.owned})`); - if (release.owned) { - showToast(`Album details coming soon!`, "info"); - } else { - showToast(`Add to download queue coming soon!`, "info"); + + // For owned/complete releases, show info message + if (release.owned && (!release.track_completion || + (typeof release.track_completion === 'object' && release.track_completion.missing_tracks === 0) || + (typeof release.track_completion === 'number' && release.track_completion === 100))) { + showToast(`${release.title} is already complete in your library`, "info"); + return; + } + + // For missing or incomplete releases, open wishlist modal + try { + // Convert release object to album format expected by our function + const albumData = { + id: release.spotify_id || release.id, + name: release.title, + image_url: release.image_url, + release_date: release.year ? `${release.year}-01-01` : '', + album_type: release.type || 'album', + total_tracks: (release.track_completion && typeof release.track_completion === 'object') + ? release.track_completion.total_tracks : 1 + }; + + // Get current artist from artist detail page state + const currentArtist = artistDetailPageState.currentArtistName ? { + id: artistDetailPageState.currentArtistId, + name: artistDetailPageState.currentArtistName, + image_url: getArtistImageFromPage() || '' // Get artist image from page + } : null; + + if (!currentArtist) { + console.error('❌ No current artist found for release click'); + showToast('Error: No artist information available', 'error'); + return; + } + + // Load tracks for the album + const response = await fetch(`/api/artist/${currentArtist.id}/album/${albumData.id}/tracks`); + if (!response.ok) { + throw new Error(`Failed to load album tracks: ${response.status}`); + } + + const data = await response.json(); + if (!data.success || !data.tracks || data.tracks.length === 0) { + throw new Error('No tracks found for this release'); + } + + // Determine album type based on release data + const albumType = release.type === 'single' ? 'singles' : 'albums'; + + // Open the Add to Wishlist modal + await openAddToWishlistModal(albumData, currentArtist, data.tracks, albumType); + + } catch (error) { + console.error('❌ Error handling release click:', error); + showToast(`Error opening wishlist modal: ${error.message}`, 'error'); } }); return card; } +/** + * Helper function to get artist image from the current artist detail page + */ +function getArtistImageFromPage() { + try { + // Try to get from artist detail image element + const artistDetailImage = document.getElementById('artist-detail-image'); + if (artistDetailImage && artistDetailImage.src && artistDetailImage.src !== window.location.href) { + return artistDetailImage.src; + } + + // Try to get from artist hero image + const artistImage = document.getElementById('artist-image'); + if (artistImage) { + const bgImage = window.getComputedStyle(artistImage).backgroundImage; + if (bgImage && bgImage !== 'none') { + // Extract URL from CSS background-image + const urlMatch = bgImage.match(/url\(["']?(.*?)["']?\)/); + if (urlMatch && urlMatch[1]) { + return urlMatch[1]; + } + } + } + + return null; + } catch (error) { + console.warn('Error getting artist image from page:', error); + return null; + } +} + // UI state management functions function showArtistDetailLoading(show) { const loadingElement = document.getElementById("artist-detail-loading"); diff --git a/webui/static/style.css b/webui/static/style.css index 71e1774c..af4e25d3 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -9543,3 +9543,541 @@ body { gap: 12px; } } + +/* ================================================ + ADD TO WISHLIST MODAL STYLES + ================================================ */ + +/* Modal Overlay and Container */ +.add-to-wishlist-modal { + display: flex; + flex-direction: column; + width: 900px; + max-width: 90vw; + max-height: 85vh; + border-radius: 20px; + overflow: hidden; + position: relative; + + /* Premium glassmorphic foundation matching download modal */ + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.95) 0%, + rgba(12, 12, 12, 0.98) 100%); + backdrop-filter: blur(20px) saturate(1.2); + + /* Enhanced borders */ + border: 1px solid rgba(255, 255, 255, 0.12); + + /* Premium shadow effect matching download modal */ + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.6), + 0 8px 32px rgba(0, 0, 0, 0.4), + 0 0 40px rgba(29, 185, 84, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + + transform: scale(0.9); + opacity: 0; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +#add-to-wishlist-modal-overlay:not(.hidden) .add-to-wishlist-modal { + transform: scale(1); + opacity: 1; +} + +/* Modal Header */ +.add-to-wishlist-modal-header { + /* Enhanced gradient with subtle transparency and glow */ + background: linear-gradient(135deg, + rgba(45, 45, 45, 0.8) 0%, + rgba(26, 26, 26, 0.9) 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + border-top-left-radius: 20px; + border-top-right-radius: 20px; + position: relative; + overflow: hidden; +} + +/* Hero Section */ +.add-to-wishlist-modal-hero { + display: flex; + align-items: center; + padding: 20px 28px; + gap: 20px; + position: relative; + z-index: 2; + min-height: 120px; +} + +.add-to-wishlist-modal-hero-bg { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-size: cover; + background-position: center; + opacity: 0.3; + filter: blur(20px); + z-index: 1; +} + +.add-to-wishlist-modal-hero-bg::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, + rgba(26, 26, 26, 0.7) 0%, + rgba(18, 18, 18, 0.8) 100%); +} + +.add-to-wishlist-modal-hero-content { + display: flex; + align-items: center; + gap: 20px; + position: relative; + z-index: 2; + width: 100%; +} + +.add-to-wishlist-modal-hero-images { + display: flex; + align-items: center; + gap: 16px; + flex-shrink: 0; +} + +.add-to-wishlist-modal-hero-image { + border-radius: 12px; + object-fit: cover; + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.4), + 0 4px 12px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.add-to-wishlist-modal-hero-image:hover { + transform: scale(1.05); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.5), + 0 6px 16px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.add-to-wishlist-modal-hero-image.artist { + border-radius: 50%; + width: 70px; + height: 70px; +} + +.add-to-wishlist-modal-hero-image.album { + border-radius: 12px; + width: 80px; + height: 80px; +} + +.add-to-wishlist-modal-hero-icon { + width: 80px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; + font-size: 36px; + border-radius: 12px; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0.05) 100%); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +.add-to-wishlist-modal-hero-metadata { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.add-to-wishlist-modal-hero-title { + color: #ffffff; + font-size: 22px; + font-weight: 700; + margin: 0; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.add-to-wishlist-modal-hero-subtitle { + color: #e0e0e0; + font-size: 16px; + font-weight: 500; + margin: 0; + opacity: 0.9; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.add-to-wishlist-modal-hero-details { + display: flex; + gap: 16px; + font-size: 14px; + margin-top: 4px; +} + +.add-to-wishlist-modal-hero-detail { + color: #1ed760; + font-weight: 600; + padding: 4px 8px; + background: rgba(29, 185, 84, 0.1); + border-radius: 6px; + border: 1px solid rgba(29, 185, 84, 0.2); + box-shadow: 0 2px 6px rgba(29, 185, 84, 0.1); + transition: all 0.3s ease; +} + +/* Header Actions */ +.add-to-wishlist-modal-header-actions { + position: absolute; + top: 16px; + right: 24px; + z-index: 3; +} + +.add-to-wishlist-modal-close { + color: #b3b3b3; + font-size: 28px; + font-weight: 300; + cursor: pointer; + transition: all 0.3s ease; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); +} + +.add-to-wishlist-modal-close:hover { + color: #ffffff; + background: rgba(255, 255, 255, 0.15); + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* Modal Content */ +.add-to-wishlist-modal-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.add-to-wishlist-modal-body { + flex: 1; + padding: 24px 28px; + overflow-y: auto; + background: #121212; +} + +.add-to-wishlist-modal-body::-webkit-scrollbar { + width: 8px; +} + +.add-to-wishlist-modal-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; +} + +.add-to-wishlist-modal-body::-webkit-scrollbar-thumb { + background: rgba(29, 185, 84, 0.3); + border-radius: 4px; +} + +.add-to-wishlist-modal-body::-webkit-scrollbar-thumb:hover { + background: rgba(29, 185, 84, 0.5); +} + +/* Track List Container */ +.wishlist-track-list-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.wishlist-track-list-header { + text-align: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 20px; +} + +.wishlist-track-list-header h3 { + color: #ffffff; + font-size: 20px; + font-weight: 700; + margin: 0 0 8px 0; +} + +.wishlist-track-list-subtitle { + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + margin: 0; +} + +/* Track List */ +.wishlist-track-list { + display: flex; + flex-direction: column; + gap: 12px; + max-height: 400px; + overflow-y: auto; + padding-right: 8px; +} + +.wishlist-track-list::-webkit-scrollbar { + width: 6px; +} + +.wishlist-track-list::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} + +.wishlist-track-list::-webkit-scrollbar-thumb { + background: rgba(29, 185, 84, 0.3); + border-radius: 3px; +} + +.wishlist-track-item { + display: flex; + align-items: center; + padding: 12px 16px; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.05) 0%, + rgba(255, 255, 255, 0.02) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + gap: 12px; +} + +.wishlist-track-item:hover { + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.08) 0%, + rgba(29, 185, 84, 0.03) 100%); + border-color: rgba(29, 185, 84, 0.2); + transform: translateX(4px); +} + +.wishlist-track-number { + width: 30px; + text-align: center; + color: rgba(255, 255, 255, 0.6); + font-size: 14px; + font-weight: 500; +} + +.wishlist-track-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.wishlist-track-name { + color: #ffffff; + font-size: 15px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wishlist-track-artists { + color: rgba(255, 255, 255, 0.7); + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wishlist-track-duration { + color: rgba(255, 255, 255, 0.6); + font-size: 13px; + font-weight: 500; + min-width: 40px; + text-align: right; +} + +/* Modal Footer */ +.add-to-wishlist-modal-footer { + background: linear-gradient(135deg, + rgba(42, 42, 42, 0.9) 0%, + rgba(30, 30, 30, 0.95) 100%); + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding: 24px 28px; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; +} + +.wishlist-modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.wishlist-modal-btn { + padding: 12px 24px; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + min-width: 120px; +} + +.wishlist-modal-btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.wishlist-modal-btn-secondary:hover { + background: rgba(255, 255, 255, 0.2); + color: #ffffff; + border-color: rgba(255, 255, 255, 0.3); +} + +.wishlist-modal-btn-primary { + background: linear-gradient(135deg, #1db954 0%, #1ed760 100%); + color: #ffffff; + border: 1px solid rgba(29, 185, 84, 0.3); +} + +.wishlist-modal-btn-primary:hover { + background: linear-gradient(135deg, #1ed760 0%, #22e167 100%); + transform: translateY(-1px); + box-shadow: 0 8px 24px rgba(29, 185, 84, 0.4); +} + +.wishlist-modal-btn-primary:active { + transform: translateY(0); + box-shadow: 0 4px 12px rgba(29, 185, 84, 0.3); +} + +/* Loading State */ +.wishlist-modal-btn-primary.loading { + background: rgba(255, 193, 7, 0.9); + color: #000000; + animation: pulse-loading 1.5s infinite; + cursor: not-allowed; +} + +.wishlist-modal-btn-primary:disabled { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.4); + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* Animation for modal appearance */ +@keyframes wishlistModalFadeIn { + from { + opacity: 0; + transform: scale(0.9) translateY(20px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +#add-to-wishlist-modal-overlay:not(.hidden) .add-to-wishlist-modal { + animation: wishlistModalFadeIn 0.4s ease-out; +} + +/* Mobile Responsiveness */ +@media (max-width: 768px) { + .add-to-wishlist-modal { + width: 95vw; + max-height: 90vh; + border-radius: 16px; + } + + .add-to-wishlist-modal-hero { + flex-direction: column; + text-align: center; + min-height: auto; + padding: 20px; + gap: 16px; + } + + .add-to-wishlist-modal-hero-content { + flex-direction: column; + gap: 16px; + } + + .add-to-wishlist-modal-hero-images { + justify-content: center; + } + + .add-to-wishlist-modal-hero-image.album, + .add-to-wishlist-modal-hero-image.artist { + width: 60px; + height: 60px; + } + + .add-to-wishlist-modal-hero-icon { + width: 60px; + height: 60px; + font-size: 28px; + margin: 0 auto; + } + + .add-to-wishlist-modal-hero-title { + font-size: 18px; + white-space: normal; + text-align: center; + } + + .add-to-wishlist-modal-hero-subtitle { + font-size: 14px; + white-space: normal; + text-align: center; + } + + .add-to-wishlist-modal-hero-details { + justify-content: center; + flex-wrap: wrap; + } + + .add-to-wishlist-modal-body { + padding: 20px; + } + + .add-to-wishlist-modal-footer { + padding: 20px; + } + + .wishlist-modal-actions { + flex-direction: column; + gap: 12px; + } + + .wishlist-modal-btn { + width: 100%; + } +}