add to wishlist functionality for library page

This commit is contained in:
Broque Thomas 2025-09-24 22:30:38 -07:00
parent ffdee23cb0
commit 09d633f80b
4 changed files with 1139 additions and 14 deletions

View file

@ -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."""

View file

@ -1241,7 +1241,47 @@
</div>
</div>
</div>
<!-- Add to Wishlist Modal -->
<div class="modal-overlay hidden" id="add-to-wishlist-modal-overlay">
<div class="add-to-wishlist-modal" id="add-to-wishlist-modal">
<div class="add-to-wishlist-modal-header">
<div class="add-to-wishlist-modal-hero" id="add-to-wishlist-modal-hero">
<!-- Hero content will be dynamically populated -->
</div>
<div class="add-to-wishlist-modal-header-actions">
<span class="add-to-wishlist-modal-close" onclick="closeAddToWishlistModal()">&times;</span>
</div>
</div>
<div class="add-to-wishlist-modal-content">
<div class="add-to-wishlist-modal-body">
<div class="wishlist-track-list-container">
<div class="wishlist-track-list-header">
<h3>Tracks to Add to Wishlist</h3>
<p class="wishlist-track-list-subtitle">All tracks from this release will be added to your wishlist</p>
</div>
<div class="wishlist-track-list" id="wishlist-track-list">
<!-- Track list will be dynamically populated -->
</div>
</div>
</div>
<div class="add-to-wishlist-modal-footer">
<div class="wishlist-modal-actions">
<button class="wishlist-modal-btn wishlist-modal-btn-secondary" onclick="closeAddToWishlistModal()">
Close
</button>
<button class="wishlist-modal-btn wishlist-modal-btn-primary" id="confirm-add-to-wishlist-btn">
Add to Wishlist
</button>
</div>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View file

@ -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 = `<div class="add-to-wishlist-modal-hero-bg" style="background-image: url('${albumImage}');"></div>`;
}
const heroContent = `
<div class="add-to-wishlist-modal-hero-content">
<div class="add-to-wishlist-modal-hero-images">
${artistImage ? `<img class="add-to-wishlist-modal-hero-image artist" src="${artistImage}" alt="${escapeHtml(artist.name)}">` : ''}
${albumImage ? `<img class="add-to-wishlist-modal-hero-image album" src="${albumImage}" alt="${escapeHtml(album.name)}">` : ''}
</div>
<div class="add-to-wishlist-modal-hero-metadata">
<h1 class="add-to-wishlist-modal-hero-title">${escapeHtml(album.name || 'Unknown Album')}</h1>
<div class="add-to-wishlist-modal-hero-subtitle">by ${escapeHtml(artist.name || 'Unknown Artist')}</div>
<div class="add-to-wishlist-modal-hero-details">
<span class="add-to-wishlist-modal-hero-detail">${albumType || 'Album'}</span>
<span class="add-to-wishlist-modal-hero-detail">${trackCount} track${trackCount !== 1 ? 's' : ''}</span>
</div>
</div>
</div>
`;
return `
${heroBackgroundImage}
${heroContent}
`;
}
/**
* Generate the track list HTML for the wishlist modal
*/
function generateWishlistTrackList(tracks) {
if (!tracks || tracks.length === 0) {
return '<div style="text-align: center; padding: 40px; color: rgba(255, 255, 255, 0.6);">No tracks found</div>';
}
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 `
<div class="wishlist-track-item">
<div class="wishlist-track-number">${trackNumber}</div>
<div class="wishlist-track-info">
<div class="wishlist-track-name">${trackName}</div>
<div class="wishlist-track-artists">${artistsString}</div>
</div>
<div class="wishlist-track-duration">${duration}</div>
</div>
`;
}).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");

View file

@ -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%;
}
}