From 871dc6cb1c6ef3e43e1414793f0dc0d288d62132 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 4 Sep 2025 13:23:54 -0700 Subject: [PATCH] Add artist album download management UI Introduces backend API and frontend integration for downloading missing tracks from artist albums and singles. Adds artist download bubbles, management modal, and related state/UI logic to script.js, with supporting styles in style.css. Also refactors some type hints in web_server.py for consistency. --- web_server.py | 78 ++++- webui/static/script.js | 638 ++++++++++++++++++++++++++++++++++++++++- webui/static/style.css | 370 +++++++++++++++++++++++- 3 files changed, 1080 insertions(+), 6 deletions(-) diff --git a/web_server.py b/web_server.py index 6c042a09..a2551791 100644 --- a/web_server.py +++ b/web_server.py @@ -1692,6 +1692,80 @@ def get_artist_discography(artist_id): traceback.print_exc() return jsonify({"error": str(e)}), 500 +@app.route('/api/artist//album//tracks', methods=['GET']) +def get_artist_album_tracks(artist_id, album_id): + """Get tracks for specific album formatted for download missing tracks modal""" + try: + if not spotify_client or not spotify_client.is_authenticated(): + return jsonify({"error": "Spotify not authenticated"}), 401 + + print(f"๐ŸŽต Fetching tracks for album: {album_id} by artist: {artist_id}") + + # Get album information first + album_data = spotify_client.get_album(album_id) + if not album_data: + return jsonify({"error": "Album not found"}), 404 + + # Get album tracks + tracks_data = spotify_client.get_album_tracks(album_id) + if not tracks_data or 'items' not in tracks_data: + return jsonify({"error": "No tracks found for album"}), 404 + + # Handle both dict and object responses from spotify_client.get_album() + if isinstance(album_data, dict): + album_info = { + 'id': album_data.get('id'), + 'name': album_data.get('name'), + 'image_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None, + 'release_date': album_data.get('release_date'), + 'album_type': album_data.get('album_type'), + 'total_tracks': album_data.get('total_tracks') + } + else: + # Handle Album object case + album_info = { + 'id': album_data.id, + 'name': album_data.name, + 'image_url': album_data.image_url, + 'release_date': album_data.release_date, + 'album_type': album_data.album_type, + 'total_tracks': album_data.total_tracks + } + + # Format tracks for download missing tracks modal compatibility + formatted_tracks = [] + for track_item in tracks_data['items']: + # Create track object compatible with download missing tracks modal + formatted_track = { + 'id': track_item['id'], + 'name': track_item['name'], + 'artists': [artist['name'] for artist in track_item['artists']], + 'duration_ms': track_item['duration_ms'], + 'track_number': track_item['track_number'], + 'disc_number': track_item.get('disc_number', 1), + 'explicit': track_item.get('explicit', False), + 'preview_url': track_item.get('preview_url'), + 'external_urls': track_item.get('external_urls', {}), + 'uri': track_item['uri'], + # Add album context for virtual playlist + 'album': album_info + } + formatted_tracks.append(formatted_track) + + print(f"โœ… Successfully formatted {len(formatted_tracks)} tracks for album: {album_info['name']}") + + return jsonify({ + 'success': True, + 'album': album_info, + 'tracks': formatted_tracks + }) + + except Exception as e: + print(f"โŒ Error fetching album tracks: {e}") + import traceback + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + @app.route('/api/artist//completion', methods=['POST']) def check_artist_discography_completion(artist_id): """Check completion status for artist's albums and singles""" @@ -1752,7 +1826,7 @@ def check_artist_discography_completion(artist_id): traceback.print_exc() return jsonify({"error": str(e)}), 500 -def _check_album_completion(db: 'MusicDatabase', album_data: dict, artist_name: str, test_mode: bool = False) -> dict: +def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: bool = False) -> dict: """Check completion status for a single album""" try: album_name = album_data.get('name', '') @@ -1837,7 +1911,7 @@ def _check_album_completion(db: 'MusicDatabase', album_data: dict, artist_name: "found_in_db": False } -def _check_single_completion(db: 'MusicDatabase', single_data: dict, artist_name: str, test_mode: bool = False) -> dict: +def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: bool = False) -> dict: """Check completion status for a single/EP (treat EPs like albums, singles as single tracks)""" try: single_name = single_data.get('name', '') diff --git a/webui/static/script.js b/webui/static/script.js index 1f5f8db3..d49a306e 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -65,6 +65,10 @@ let artistsPageState = { }, isInitialized: false // Track if the page has been initialized }; + +// --- Artist Downloads Management State --- +let artistDownloadBubbles = {}; // Track artist download bubbles: artistId -> { artist, downloads: [], element } +let artistDownloadModalOpen = false; // Track if artist download modal is open let artistsSearchTimeout = null; let artistsSearchController = null; @@ -3115,6 +3119,11 @@ async function closeDownloadMissingModal(playlistId) { console.log('๐Ÿ“ฑ [Modal State] Cleared wishlist modal state on full close'); } + // Clean up artist download if this is an artist album playlist + if (playlistId.startsWith('artist_album_')) { + cleanupArtistDownload(playlistId); + } + cleanupDownloadProcess(playlistId); } } @@ -5254,6 +5263,11 @@ window.handleWishlistButtonClick = handleWishlistButtonClick; window.escapeHtml = escapeHtml; window.formatArtists = formatArtists; +// Artist Download Management functions +window.closeArtistDownloadModal = closeArtistDownloadModal; +window.openArtistDownloadProcess = openArtistDownloadProcess; +window.bulkCompleteArtistDownloads = bulkCompleteArtistDownloads; + // APPEND THIS JAVASCRIPT SNIPPET (B) @@ -9523,7 +9537,7 @@ function displayArtistDiscography(discography) { if (discography.albums?.length > 0) { albumsContainer.innerHTML = discography.albums.map(album => createAlbumCard(album)).join(''); - // Add dynamic glow effects to album cards + // Add dynamic glow effects and click handlers to album cards albumsContainer.querySelectorAll('.album-card').forEach((card, index) => { const album = discography.albums[index]; if (album.image_url) { @@ -9531,6 +9545,10 @@ function displayArtistDiscography(discography) { applyDynamicGlow(card, colors); }); } + + // Add click handler for download missing tracks modal + card.addEventListener('click', () => handleArtistAlbumClick(album, 'albums')); + card.style.cursor = 'pointer'; }); } else { albumsContainer.innerHTML = ` @@ -9548,7 +9566,7 @@ function displayArtistDiscography(discography) { if (discography.singles?.length > 0) { singlesContainer.innerHTML = discography.singles.map(single => createAlbumCard(single)).join(''); - // Add dynamic glow effects to singles cards + // Add dynamic glow effects and click handlers to singles cards singlesContainer.querySelectorAll('.album-card').forEach((card, index) => { const single = discography.singles[index]; if (single.image_url) { @@ -9556,6 +9574,10 @@ function displayArtistDiscography(discography) { applyDynamicGlow(card, colors); }); } + + // Add click handler for download missing tracks modal + card.addEventListener('click', () => handleArtistAlbumClick(single, 'singles')); + card.style.cursor = 'pointer'; }); } else { singlesContainer.innerHTML = ` @@ -9889,6 +9911,9 @@ function showArtistsSearchState() { artistsPageState.currentView = 'search'; updateArtistsSearchStatus('default'); + + // Show artist downloads section if there are active downloads + showArtistDownloadsSection(); } function showArtistsResultsState() { @@ -10096,6 +10121,615 @@ function showSearchLoadingCards() { container.innerHTML = loadingCardHtml.repeat(6); } +// =============================== +// ARTIST ALBUM DOWNLOAD MISSING TRACKS INTEGRATION +// =============================== + +/** + * 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 { + // 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}`); + const process = activeDownloadProcesses[virtualPlaylistId]; + if (process.modalElement) { + if (process.status === 'complete') { + showToast('Showing previous results. Close this modal to start a new analysis.', 'info'); + } + process.modalElement.style.display = 'flex'; + return; + } + } + + // Create virtual playlist and open modal + await createArtistAlbumVirtualPlaylist(album, albumType); + + } catch (error) { + console.error('โŒ Error opening download missing tracks modal:', error); + showToast(`Error opening download modal: ${error.message}`, 'error'); + } +} + +/** + * Create virtual playlist for artist album and open download missing tracks modal + */ +async function createArtistAlbumVirtualPlaylist(album, albumType) { + const artist = artistsPageState.selectedArtist; + const virtualPlaylistId = `artist_album_${artist.id}_${album.id}`; + + console.log(`๐ŸŽต Creating virtual playlist for: ${artist.name} - ${album.name}`); + + try { + // Show loading toast + showToast(`Loading tracks for ${album.name}...`, 'info'); + + // Fetch album tracks from backend + const response = await fetch(`/api/artist/${artist.id}/album/${album.id}/tracks`); + + if (!response.ok) { + if (response.status === 401) { + throw new Error('Spotify not authenticated. Please check your API settings.'); + } + 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'); + } + + console.log(`โœ… Loaded ${data.tracks.length} tracks for ${album.name}`); + + // Format playlist name with artist and album info + const playlistName = `[${artist.name}] ${album.name}`; + + // Open download missing tracks modal with formatted tracks + await openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, data.tracks, album, artist); + + // Track this download for artist bubble management + registerArtistDownload(artist, album, virtualPlaylistId, albumType); + + } catch (error) { + console.error('โŒ Error creating virtual playlist:', error); + showToast(`Failed to load album: ${error.message}`, 'error'); + throw error; + } +} + +/** + * Open download missing tracks modal specifically for artist albums + * Similar to openDownloadMissingModalForYouTube but for artist albums + */ +async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, spotifyTracks, album, artist) { + // Check if a process is already active for this virtual playlist + if (activeDownloadProcesses[virtualPlaylistId]) { + console.log(`Modal for ${virtualPlaylistId} already exists. Showing it.`); + const process = activeDownloadProcesses[virtualPlaylistId]; + if (process.modalElement) { + if (process.status === 'complete') { + showToast('Showing previous results. Close this modal to start a new analysis.', 'info'); + } + process.modalElement.style.display = 'flex'; + } + return; + } + + console.log(`๐Ÿ“ฅ Opening Download Missing Tracks modal for artist album: ${virtualPlaylistId}`); + + // Create virtual playlist object for compatibility with existing modal logic + const virtualPlaylist = { + id: virtualPlaylistId, + name: playlistName, + track_count: spotifyTracks.length + }; + + // Store the tracks in the cache for the modal to use + playlistTrackCache[virtualPlaylistId] = spotifyTracks; + currentPlaylistTracks = spotifyTracks; + currentModalPlaylistId = virtualPlaylistId; + + let modal = document.createElement('div'); + modal.id = `download-missing-modal-${virtualPlaylistId}`; + modal.className = 'download-missing-modal'; + modal.style.display = 'none'; + document.body.appendChild(modal); + + // Register the new process in our global state tracker using the same structure as other modals + activeDownloadProcesses[virtualPlaylistId] = { + status: 'idle', + modalElement: modal, + poller: null, + batchId: null, + playlist: virtualPlaylist, + tracks: spotifyTracks, + // Additional metadata for artist albums + artist: artist, + album: album, + albumType: album.album_type + }; + + // Use the exact same modal HTML structure as the existing modals + modal.innerHTML = ` +
+
+

Download Missing Tracks - ${escapeHtml(playlistName)}

+ × +
+ +
+
+
+
${spotifyTracks.length}
+
Total Tracks
+
+
+
-
+
Found in Library
+
+
+
-
+
Missing Tracks
+
+
+
0
+
Downloaded
+
+
+ +
+
+
+ ๐Ÿ” Library Analysis + Ready to start +
+
+
+
+
+
+
+ โฌ Downloads + Waiting for analysis +
+
+
+
+
+
+ +
+
+

๐Ÿ“‹ Track Analysis & Download Status

+
+
+ + + + + + + + + + + + + + ${spotifyTracks.map((track, index) => ` + + + + + + + + + + `).join('')} + +
#Track NameArtist(s)DurationLibrary StatusDownload StatusActions
${index + 1}${escapeHtml(track.name)}${track.artists.join(', ')}${formatDuration(track.duration_ms)}๐Ÿ” Pending--
+
+
+
+ + +
+ `; + + modal.style.display = 'flex'; + + console.log(`โœ… Successfully opened download missing tracks modal for: ${playlistName}`); +} + +// =============================== +// ARTIST DOWNLOADS MANAGEMENT SYSTEM +// =============================== + +/** + * Register a new artist download for bubble management + */ +function registerArtistDownload(artist, album, virtualPlaylistId, albumType) { + console.log(`๐Ÿ“ Registering artist download: ${artist.name} - ${album.name}`); + + const artistId = artist.id; + + // Initialize artist bubble if it doesn't exist + if (!artistDownloadBubbles[artistId]) { + artistDownloadBubbles[artistId] = { + artist: artist, + downloads: [], + element: null, + hasCompletedDownloads: false + }; + } + + // Add this download to the artist's downloads + const downloadInfo = { + virtualPlaylistId: virtualPlaylistId, + album: album, + albumType: albumType, + status: 'in_progress', // 'in_progress', 'completed', 'view_results' + startTime: new Date() + }; + + artistDownloadBubbles[artistId].downloads.push(downloadInfo); + + // Show/update the artist downloads section + showArtistDownloadsSection(); + + // Monitor this download for completion + monitorArtistDownload(artistId, virtualPlaylistId); +} + +/** + * Show or update the artist downloads section in search state + */ +function showArtistDownloadsSection() { + // Only show in search state + if (artistsPageState.currentView !== 'search') { + return; + } + + const artistsSearchState = document.getElementById('artists-search-state'); + if (!artistsSearchState) return; + + let downloadsSection = document.getElementById('artist-downloads-section'); + + // Create section if it doesn't exist + if (!downloadsSection) { + downloadsSection = document.createElement('div'); + downloadsSection.id = 'artist-downloads-section'; + downloadsSection.className = 'artist-downloads-section'; + + // Insert after the search container + const searchContainer = artistsSearchState.querySelector('.artists-search-container'); + if (searchContainer) { + searchContainer.insertAdjacentElement('afterend', downloadsSection); + } + } + + // Count active artists (those with downloads) + const activeArtists = Object.keys(artistDownloadBubbles).filter(artistId => + artistDownloadBubbles[artistId].downloads.length > 0 + ); + + if (activeArtists.length === 0) { + downloadsSection.style.display = 'none'; + return; + } + + // Show and populate the section + downloadsSection.style.display = 'block'; + downloadsSection.innerHTML = ` +
+

Current Downloads

+

Active download processes

+
+
+ ${activeArtists.map(artistId => createArtistBubbleCard(artistDownloadBubbles[artistId])).join('')} +
+ `; + + // Add event listeners to bubble cards + activeArtists.forEach(artistId => { + const bubbleCard = downloadsSection.querySelector(`[data-artist-id="${artistId}"]`); + if (bubbleCard) { + bubbleCard.addEventListener('click', () => openArtistDownloadModal(artistId)); + + // Add dynamic glow effect + const artist = artistDownloadBubbles[artistId].artist; + if (artist.image_url) { + extractImageColors(artist.image_url, (colors) => { + applyDynamicGlow(bubbleCard, colors); + }); + } + } + }); +} + +/** + * Create HTML for an artist bubble card + */ +function createArtistBubbleCard(artistBubbleData) { + const { artist, downloads } = artistBubbleData; + const activeCount = downloads.filter(d => d.status === 'in_progress').length; + const completedCount = downloads.filter(d => d.status === 'view_results').length; + const allCompleted = activeCount === 0 && completedCount > 0; + + const imageUrl = artist.image_url || ''; + const backgroundStyle = imageUrl ? + `background-image: url('${imageUrl}');` : + `background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);`; + + return ` +
+
+
+
+
${escapeHtml(artist.name)}
+
+ ${activeCount > 0 ? `${activeCount} active` : ''} + ${completedCount > 0 ? `${completedCount} completed` : ''} +
+
+ ${allCompleted ? ` +
+ โœ… +
+ ` : ''} +
+ `; +} + +/** + * Monitor an artist download for completion status changes + */ +function monitorArtistDownload(artistId, virtualPlaylistId) { + // Check if the download process exists and monitor its status + const checkStatus = () => { + const process = activeDownloadProcesses[virtualPlaylistId]; + if (!process || !artistDownloadBubbles[artistId]) { + return; // Process or artist bubble no longer exists + } + + // Find this download in the artist's downloads + const download = artistDownloadBubbles[artistId].downloads.find(d => d.virtualPlaylistId === virtualPlaylistId); + if (!download) return; + + // Update download status based on process status + if (process.status === 'complete' && download.status === 'in_progress') { + download.status = 'view_results'; + console.log(`โœ… Download completed for ${artistDownloadBubbles[artistId].artist.name} - ${download.album.name}`); + + // Update the downloads section + showArtistDownloadsSection(); + } + + // Continue monitoring if still active + if (process.status !== 'complete') { + setTimeout(checkStatus, 2000); // Check every 2 seconds + } + }; + + // Start monitoring after a brief delay + setTimeout(checkStatus, 1000); +} + +/** + * Open the artist download management modal + */ +function openArtistDownloadModal(artistId) { + const artistBubbleData = artistDownloadBubbles[artistId]; + if (!artistBubbleData || artistDownloadModalOpen) return; + + console.log(`๐ŸŽต Opening artist download modal for: ${artistBubbleData.artist.name}`); + artistDownloadModalOpen = true; + + const modal = document.createElement('div'); + modal.id = 'artist-download-management-modal'; + modal.className = 'artist-download-management-modal'; + modal.innerHTML = ` +
+
+

Downloads for ${escapeHtml(artistBubbleData.artist.name)}

+ × +
+ +
+
+ ${artistBubbleData.downloads.map((download, index) => createArtistDownloadItem(download, index)).join('')} +
+
+
+
+ `; + + document.body.appendChild(modal); + modal.style.display = 'flex'; + + // Monitor for real-time updates + startArtistDownloadModalMonitoring(artistId); +} + +/** + * Create HTML for an individual download item in the artist modal + */ +function createArtistDownloadItem(download, index) { + const { album, albumType, status, virtualPlaylistId } = download; + const buttonText = status === 'view_results' ? 'View Results' : 'View Progress'; + const buttonClass = status === 'view_results' ? 'completed' : 'active'; + + return ` +
+
+
${escapeHtml(album.name)}
+
${albumType === 'album' ? 'Album' : albumType === 'single' ? 'Single' : 'EP'}
+
+
+ +
+
+ `; +} + +/** + * Monitor artist download modal for real-time updates + */ +function startArtistDownloadModalMonitoring(artistId) { + if (!artistDownloadModalOpen) return; + + const updateModal = () => { + const modal = document.getElementById('artist-download-management-modal'); + const itemsContainer = document.getElementById(`artist-download-items-${artistId}`); + + if (!modal || !itemsContainer || !artistDownloadBubbles[artistId]) return; + + // Check for completed downloads that need to be removed + const activeDownloads = artistDownloadBubbles[artistId].downloads.filter(download => { + const process = activeDownloadProcesses[download.virtualPlaylistId]; + // Keep if process exists or if it's completed but not yet cleaned up + return process !== undefined; + }); + + // Update the downloads array + artistDownloadBubbles[artistId].downloads = activeDownloads; + + // If no downloads left, close modal + if (activeDownloads.length === 0) { + closeArtistDownloadModal(); + return; + } + + // Update modal content + itemsContainer.innerHTML = activeDownloads.map((download, index) => { + const process = activeDownloadProcesses[download.virtualPlaylistId]; + if (process) { + download.status = process.status === 'complete' ? 'view_results' : 'in_progress'; + } + return createArtistDownloadItem(download, index); + }).join(''); + + // Continue monitoring + setTimeout(updateModal, 2000); + }; + + setTimeout(updateModal, 1000); +} + +/** + * Open a specific artist download process modal + */ +function openArtistDownloadProcess(virtualPlaylistId) { + const process = activeDownloadProcesses[virtualPlaylistId]; + if (process && process.modalElement) { + // Close artist management modal first + closeArtistDownloadModal(); + + // Show the download process modal + process.modalElement.style.display = 'flex'; + + if (process.status === 'complete') { + showToast('Review download results and click "Close" to finish.', 'info'); + } + } +} + +/** + * Close the artist download management modal + */ +function closeArtistDownloadModal() { + const modal = document.getElementById('artist-download-management-modal'); + if (modal) { + modal.remove(); + } + artistDownloadModalOpen = false; +} + +/** + * Bulk complete all downloads for an artist (when all are in 'view_results' state) + */ +function bulkCompleteArtistDownloads(artistId) { + console.log(`๐ŸŽฏ Bulk completing downloads for artist: ${artistId}`); + + const artistBubbleData = artistDownloadBubbles[artistId]; + if (!artistBubbleData) return; + + // Find all downloads in 'view_results' state + const completedDownloads = artistBubbleData.downloads.filter(d => d.status === 'view_results'); + + // Programmatically close all completed modals + completedDownloads.forEach(download => { + const process = activeDownloadProcesses[download.virtualPlaylistId]; + if (process && process.modalElement) { + // Trigger the close function which handles cleanup + closeDownloadMissingModal(download.virtualPlaylistId); + } + }); + + showToast(`Completed ${completedDownloads.length} downloads for ${artistBubbleData.artist.name}`, 'success'); +} + +/** + * Clean up artist download when a modal is closed + */ +function cleanupArtistDownload(virtualPlaylistId) { + // Find which artist this download belongs to + for (const artistId in artistDownloadBubbles) { + const downloads = artistDownloadBubbles[artistId].downloads; + const downloadIndex = downloads.findIndex(d => d.virtualPlaylistId === virtualPlaylistId); + + if (downloadIndex !== -1) { + console.log(`๐Ÿงน Cleaning up artist download: ${downloads[downloadIndex].album.name}`); + + // Remove this download from the artist's downloads + downloads.splice(downloadIndex, 1); + + // If no more downloads for this artist, remove the bubble + if (downloads.length === 0) { + delete artistDownloadBubbles[artistId]; + console.log(`๐Ÿงน Removed artist bubble: ${artistId}`); + } + + // Update the downloads section + showArtistDownloadsSection(); + break; + } + } +} + /** * Extract dominant colors from an image for dynamic glow effects */ diff --git a/webui/static/style.css b/webui/static/style.css index ccdab5bf..a0c5e5b6 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -5634,15 +5634,18 @@ body { /* ARTISTS PAGE STYLES - ELEGANT GLASSMORPHIC DESIGN */ /* ============================================================================ */ -/* Artists Search State - Initial centered interface */ +/* Artists Search State - Initial centered interface with downloads section support */ .artists-search-state { display: flex; + flex-direction: column; align-items: center; + gap: 10vh; justify-content: center; min-height: 70vh; padding: 40px 20px; opacity: 1; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + gap: 32px; } .artists-search-state.fade-out { @@ -5654,6 +5657,14 @@ body { max-width: 600px; width: 100%; text-align: center; + margin-top: auto; + margin-bottom: auto; +} + +/* When downloads section is present, adjust positioning */ +.artists-search-state:has(.artist-downloads-section) .artists-search-container { + margin-top: 0; + margin-bottom: 0; } .artists-welcome-section { @@ -6542,4 +6553,359 @@ body { overflow-x: auto; padding-bottom: 5px; } -} \ No newline at end of file +} + +/* =============================== + ARTIST DOWNLOADS MANAGEMENT STYLES + =============================== */ + +/* Artist Downloads Section */ +.artist-downloads-section { + max-width: 800px; + padding: 0 20px; + animation: fadeInUp 0.5s ease-out; + width: 100%; +} + +.artist-downloads-header { + margin-bottom: 20px; + text-align: center; +} + +.artist-downloads-title { + font-size: 20px; + font-weight: 700; + color: rgba(255, 255, 255, 0.9); + margin: 0 0 8px; + font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; +} + +.artist-downloads-subtitle { + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + margin: 0; +} + +.artist-bubble-container { + display: flex; + flex-wrap: wrap; + gap: 16px; + justify-content: center; + align-items: center; +} + +/* Artist Bubble Card */ +.artist-bubble-card { + position: relative; + width: 80px; + height: 80px; + border-radius: 50%; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; + + background: linear-gradient(135deg, + rgba(26, 26, 26, 0.95) 0%, + rgba(18, 18, 18, 0.98) 100%); + border: 2px solid rgba(255, 255, 255, 0.1); + + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(29, 185, 84, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.06); +} + +.artist-bubble-card:hover { + transform: translateY(-3px) scale(1.05); + border-color: rgba(29, 185, 84, 0.3); + + box-shadow: + 0 8px 20px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(29, 185, 84, 0.2), + 0 0 15px rgba(29, 185, 84, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.artist-bubble-card.all-completed { + border-color: rgba(34, 197, 94, 0.4); +} + +.artist-bubble-card.all-completed:hover { + border-color: rgba(34, 197, 94, 0.6); + box-shadow: + 0 8px 20px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(34, 197, 94, 0.3), + 0 0 15px rgba(34, 197, 94, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.artist-bubble-image { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: 50%; +} + +.artist-bubble-overlay { + position: absolute; + inset: 0; + background: linear-gradient(135deg, + rgba(0, 0, 0, 0.3) 0%, + rgba(0, 0, 0, 0.6) 100%); + border-radius: 50%; +} + +.artist-bubble-content { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 8px; + text-align: center; + z-index: 2; +} + +.artist-bubble-name { + font-size: 10px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + line-height: 1.2; + margin-bottom: 2px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.artist-bubble-status { + font-size: 8px; + color: rgba(255, 255, 255, 0.7); + line-height: 1.1; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.bulk-complete-indicator { + position: absolute; + top: -4px; + right: -4px; + width: 24px; + height: 24px; + background: linear-gradient(135deg, + rgba(34, 197, 94, 0.95) 0%, + rgba(21, 128, 61, 0.95) 100%); + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 3; + transition: all 0.2s ease; + + box-shadow: + 0 2px 8px rgba(34, 197, 94, 0.4), + 0 0 0 1px rgba(34, 197, 94, 0.2); +} + +.bulk-complete-indicator:hover { + transform: scale(1.1); + box-shadow: + 0 4px 12px rgba(34, 197, 94, 0.6), + 0 0 0 1px rgba(34, 197, 94, 0.3); +} + +.bulk-complete-icon { + font-size: 12px; + color: white; +} + +/* Artist Download Management Modal */ +.artist-download-management-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease-out; +} + +.artist-download-modal-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(8px); + z-index: 1; +} + +.artist-download-modal-content { + position: relative; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.98) 0%, + rgba(12, 12, 12, 0.98) 100%); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + max-width: 500px; + width: 90%; + max-height: 70vh; + overflow: hidden; + z-index: 2; + + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.6), + 0 0 0 1px rgba(29, 185, 84, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.artist-download-modal-header { + padding: 24px 24px 0; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding-bottom: 16px; + margin-bottom: 20px; +} + +.artist-download-modal-title { + font-size: 20px; + font-weight: 700; + color: rgba(255, 255, 255, 0.95); + margin: 0; + flex: 1; + font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; +} + +.artist-download-modal-close { + font-size: 24px; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + padding: 4px; + border-radius: 6px; + transition: all 0.2s ease; + line-height: 1; +} + +.artist-download-modal-close:hover { + color: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.1); +} + +.artist-download-modal-body { + padding: 0 24px 24px; + overflow-y: auto; + max-height: calc(70vh - 100px); +} + +.artist-download-items { + display: flex; + flex-direction: column; + gap: 12px; +} + +.artist-download-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: linear-gradient(135deg, + rgba(26, 26, 26, 0.6) 0%, + rgba(18, 18, 18, 0.8) 100%); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + transition: all 0.2s ease; +} + +.artist-download-item:hover { + background: linear-gradient(135deg, + rgba(26, 26, 26, 0.8) 0%, + rgba(18, 18, 18, 0.9) 100%); + border-color: rgba(255, 255, 255, 0.1); +} + +.download-item-info { + flex: 1; +} + +.download-item-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 4px; + line-height: 1.3; +} + +.download-item-type { + font-size: 12px; + color: rgba(255, 255, 255, 0.6); + text-transform: capitalize; +} + +.download-item-actions { + margin-left: 16px; +} + +.download-item-btn { + padding: 8px 16px; + border-radius: 8px; + border: none; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + min-width: 100px; +} + +.download-item-btn.active { + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.9) 0%, + rgba(24, 156, 71, 0.9) 100%); + color: white; + box-shadow: 0 2px 8px rgba(29, 185, 84, 0.3); +} + +.download-item-btn.active:hover { + background: linear-gradient(135deg, + rgba(29, 185, 84, 1.0) 0%, + rgba(24, 156, 71, 1.0) 100%); + box-shadow: 0 4px 12px rgba(29, 185, 84, 0.4); + transform: translateY(-1px); +} + +.download-item-btn.completed { + background: linear-gradient(135deg, + rgba(34, 197, 94, 0.9) 0%, + rgba(21, 128, 61, 0.9) 100%); + color: white; + box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3); +} + +.download-item-btn.completed:hover { + background: linear-gradient(135deg, + rgba(34, 197, 94, 1.0) 0%, + rgba(21, 128, 61, 1.0) 100%); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4); + transform: translateY(-1px); +} + +/* Dynamic glow effects for artist bubble cards */ +.artist-bubble-card.has-dynamic-glow { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1), + filter 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.artist-bubble-card.has-dynamic-glow:hover { + filter: drop-shadow(0 0 6px var(--glow-color-1, #1db954)) + drop-shadow(0 0 12px var(--glow-color-2, #1ed760)); + border-color: var(--glow-color-1, rgba(29, 185, 84, 0.4)); +}