diff --git a/webui/static/script.js b/webui/static/script.js index d427e8b9..165bbe61 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -21006,6 +21006,9 @@ async function loadGenreBrowserGenres() { genresGrid.innerHTML = genreCardsHTML; + // Add click event listeners to genre cards + addGenreBrowserCardClickListeners(); + // Cache the filtered genres data genreBrowserCache.genres = filteredGenres; genreBrowserCache.lastLoaded = new Date(); @@ -21065,6 +21068,9 @@ function displayCachedGenres() { genresGrid.innerHTML = genreCardsHTML; + // Add click event listeners to genre cards + addGenreBrowserCardClickListeners(); + console.log(`βœ… Displayed ${genres.length} cached genres instantly`); // Handle image loading based on current state @@ -21221,6 +21227,442 @@ function filterGenreBrowserCards(searchTerm) { console.log(`πŸ” Filtered genre cards with search term: "${searchTerm}"`); } +// === GENRE BROWSER CARD CLICK HANDLERS === + +function addGenreBrowserCardClickListeners() { + const genreCards = document.querySelectorAll('.genre-browser-card'); + genreCards.forEach(card => { + card.addEventListener('click', () => { + const genreSlug = card.dataset.genreSlug; + const genreId = card.dataset.genreId; + const genreName = card.dataset.genreName; + + console.log(`🎡 Genre card clicked: ${genreName} (${genreSlug})`); + handleGenreBrowserCardClick(genreSlug, genreId, genreName); + }); + }); + + console.log(`πŸ”— Added click listeners to ${genreCards.length} genre browser cards`); +} + +async function handleGenreBrowserCardClick(genreSlug, genreId, genreName) { + console.log(`🎠 Loading hero slider for ${genreName}...`); + + try { + // Show the genre page view + showGenrePageView(genreSlug, genreId, genreName); + + // Load the hero slider data + await loadGenreHeroSlider(genreSlug, genreId, genreName); + + } catch (error) { + console.error(`❌ Error loading genre page for ${genreName}:`, error); + showToast(`Error loading ${genreName}: ${error.message}`, 'error'); + + // Return to genre list on error + showGenreListView(); + } +} + +function showGenrePageView(genreSlug, genreId, genreName) { + console.log(`🎯 Showing genre page view for ${genreName}`); + + // CRITICAL: Stop all other slider auto-play to prevent conflicts + if (typeof beatportRebuildSliderState !== 'undefined' && beatportRebuildSliderState.autoPlayInterval) { + clearInterval(beatportRebuildSliderState.autoPlayInterval); + console.log('πŸ›‘ Stopped main slider auto-play to prevent conflicts'); + } + + const modal = document.getElementById('genre-browser-modal'); + if (!modal) return; + + // Hide genre list elements + const searchSection = modal.querySelector('.genre-browser-search-section'); + const genresSection = modal.querySelector('.genre-browser-genres-section'); + + if (searchSection) searchSection.style.display = 'none'; + if (genresSection) genresSection.style.display = 'none'; + + // Create or show genre page content + let genrePageContent = modal.querySelector('.genre-page-content'); + if (!genrePageContent) { + genrePageContent = document.createElement('div'); + genrePageContent.className = 'genre-page-content'; + genrePageContent.innerHTML = ` +
+ +

+
+
+
+
+

🎠 Loading hero releases...

+
+
+ `; + + modal.querySelector('.genre-browser-modal-content').appendChild(genrePageContent); + + // Add back button listener + const backButton = genrePageContent.querySelector('#genre-back-button'); + if (backButton) { + backButton.addEventListener('click', showGenreListView); + } + } + + // Update title and show genre page + const titleElement = genrePageContent.querySelector('.genre-page-title'); + if (titleElement) titleElement.textContent = genreName; + + genrePageContent.style.display = 'block'; + + // Store current genre info for potential back navigation + genrePageContent.dataset.genreSlug = genreSlug; + genrePageContent.dataset.genreId = genreId; + genrePageContent.dataset.genreName = genreName; +} + +function showGenreListView() { + console.log(`πŸ”™ Returning to genre list view`); + + // Clean up genre hero slider + if (window.genreHeroSliderState && window.genreHeroSliderState.autoPlayInterval) { + clearInterval(window.genreHeroSliderState.autoPlayInterval); + console.log('🧹 Cleaned up genre hero slider auto-play'); + } + + // CRITICAL: Restart main slider auto-play + if (typeof beatportRebuildSliderState !== 'undefined' && !beatportRebuildSliderState.autoPlayInterval) { + if (typeof startBeatportRebuildSliderAutoPlay === 'function') { + startBeatportRebuildSliderAutoPlay(); + console.log('πŸ”„ Restarted main slider auto-play'); + } + } + + const modal = document.getElementById('genre-browser-modal'); + if (!modal) return; + + // Show genre list elements + const searchSection = modal.querySelector('.genre-browser-search-section'); + const genresSection = modal.querySelector('.genre-browser-genres-section'); + const genrePageContent = modal.querySelector('.genre-page-content'); + + if (searchSection) searchSection.style.display = 'block'; + if (genresSection) genresSection.style.display = 'block'; + if (genrePageContent) genrePageContent.style.display = 'none'; +} + +async function loadGenreHeroSlider(genreSlug, genreId, genreName) { + console.log(`🎠 Loading hero slider data for ${genreName}...`); + + const container = document.getElementById('genre-hero-slider-container'); + if (!container) return; + + try { + // Show loading state + container.innerHTML = ` +
+
+

🎠 Loading ${genreName} hero releases...

+
+ `; + + // Fetch hero slider data from API + const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/hero`); + if (!response.ok) { + throw new Error(`API returned ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.success || !data.releases || data.releases.length === 0) { + throw new Error(data.message || 'No hero releases found'); + } + + console.log(`βœ… Loaded ${data.count} hero releases for ${genreName} (cached: ${data.cached})`); + + // Create hero slider HTML + const heroSliderHTML = createGenreHeroSliderHTML(data.releases, genreName); + container.innerHTML = heroSliderHTML; + + // Add click handlers to individual releases (for future download functionality) + addGenreHeroReleaseClickHandlers(data.releases); + + showToast(`Loaded ${data.count} ${genreName} releases`, 'success'); + + } catch (error) { + console.error(`❌ Error loading hero slider for ${genreName}:`, error); + + container.innerHTML = ` +
+

❌ Failed to load ${genreName} releases

+

${error.message}

+ +
+ `; + + throw error; + } +} + +function createGenreHeroSliderHTML(releases, genreName) { + const slidesHTML = releases.map((release, index) => { + // Convert relative URL to absolute URL + const absoluteUrl = release.url.startsWith('http') + ? release.url + : `https://www.beatport.com${release.url}`; + + return ` +
+
+
+
+
+
+

${release.title}

+

${release.artists_string}

+

${release.label || genreName + ' Hero Release'}

+
+
+
`; + }).join(''); + + const indicatorsHTML = releases.map((_, index) => ` + + `).join(''); + + return ` +
+
+
+ ${slidesHTML} +
+ + +
+ + +
+ + +
+ ${indicatorsHTML} +
+
+
+ `; +} + +function addGenreHeroReleaseClickHandlers(releases) { + // Clear any existing intervals first + if (window.genreHeroSliderState && window.genreHeroSliderState.autoPlayInterval) { + clearInterval(window.genreHeroSliderState.autoPlayInterval); + console.log('🧹 Cleared previous genre hero auto-play interval'); + } + + // CRITICAL: Clear ALL possible conflicting intervals + if (typeof beatportRebuildSliderState !== 'undefined' && beatportRebuildSliderState.autoPlayInterval) { + clearInterval(beatportRebuildSliderState.autoPlayInterval); + console.log('πŸ›‘ Cleared main rebuild slider auto-play interval'); + } + + // Initialize global slider state for genre hero slider + window.genreHeroSliderState = { + currentSlide: 0, + totalSlides: releases.length, + autoPlayInterval: null + }; + + console.log(`🎠 Initializing genre hero slider with ${releases.length} slides`); + + // Set up navigation button handlers + const prevBtn = document.getElementById('genre-hero-prev-btn'); + const nextBtn = document.getElementById('genre-hero-next-btn'); + + if (prevBtn) { + prevBtn.addEventListener('click', () => { + window.genreHeroSliderState.currentSlide = window.genreHeroSliderState.currentSlide > 0 + ? window.genreHeroSliderState.currentSlide - 1 + : window.genreHeroSliderState.totalSlides - 1; + updateGenreHeroSlide(window.genreHeroSliderState.currentSlide); + console.log(`⬅️ Previous: Moving to slide ${window.genreHeroSliderState.currentSlide}`); + }); + } + + if (nextBtn) { + nextBtn.addEventListener('click', () => { + window.genreHeroSliderState.currentSlide = (window.genreHeroSliderState.currentSlide + 1) % window.genreHeroSliderState.totalSlides; + updateGenreHeroSlide(window.genreHeroSliderState.currentSlide); + console.log(`➑️ Next: Moving to slide ${window.genreHeroSliderState.currentSlide}`); + }); + } + + // Set up indicator handlers + const indicators = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-indicator'); + indicators.forEach((indicator, index) => { + indicator.addEventListener('click', () => { + window.genreHeroSliderState.currentSlide = index; + updateGenreHeroSlide(index); + console.log(`🎯 Indicator: Jumping to slide ${index}`); + }); + }); + + // Set up individual slide click handlers (like the main hero slider) + const slides = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-slide[data-url]'); + console.log(`πŸ”— Found ${slides.length} slides to set up click handlers for`); + + slides.forEach((slide, index) => { + const releaseUrl = slide.getAttribute('data-url'); + if (releaseUrl && releaseUrl !== '#' && releaseUrl !== '') { + const release = releases[index]; + if (release) { + // Ensure we use the absolute URL and match the expected data structure + const releaseData = { + url: releaseUrl, // This is already the absolute URL from data-url + title: release.title || 'Unknown Title', + artist: release.artists_string || 'Unknown Artist', // handleBeatportReleaseCardClick expects 'artist' + label: release.label || 'Unknown Label', + image_url: release.image_url || '', + // Include all original data for completeness + artists_string: release.artists_string, + type: release.type, + source: release.source, + badges: release.badges || [] + }; + + slide.addEventListener('click', async (event) => { + // Prevent navigation button clicks from triggering this + if (event.target.closest('.beatport-rebuild-nav-btn') || + event.target.closest('.beatport-rebuild-indicator')) { + return; + } + + console.log(`🎡 Genre hero slide clicked: ${releaseData.title} by ${releaseData.artist}`); + + // Use the exact same functionality as the main hero slider + await handleBeatportReleaseCardClick(slide, releaseData); + }); + + slide.style.cursor = 'pointer'; + } + } + }); + + // Ensure first slide is active BEFORE starting auto-play + updateGenreHeroSlide(0); + + // Delay auto-play start to let DOM settle + setTimeout(() => { + startGenreHeroSliderAutoPlay(); + }, 100); + + // Pause on hover + const sliderContainer = document.querySelector('#genre-hero-slider'); + if (sliderContainer) { + sliderContainer.addEventListener('mouseenter', () => { + if (window.genreHeroSliderState.autoPlayInterval) { + clearInterval(window.genreHeroSliderState.autoPlayInterval); + console.log('⏸️ Paused auto-play on hover'); + } + }); + + sliderContainer.addEventListener('mouseleave', () => { + // Delay restart to avoid rapid state changes + setTimeout(() => { + startGenreHeroSliderAutoPlay(); + }, 100); + console.log('▢️ Resumed auto-play after hover'); + }); + } + + console.log(`βœ… Set up slider functionality for ${releases.length} genre hero releases`); +} + +function updateGenreHeroSlide(slideIndex) { + if (!window.genreHeroSliderState) { + console.error('❌ Genre hero slider state not initialized'); + return; + } + + // First update the state + window.genreHeroSliderState.currentSlide = slideIndex; + + // Update slide visibility - use the exact same logic as main slider + const slides = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-slide'); + console.log(`πŸ”„ Updating slide to index ${slideIndex}, found ${slides.length} slides`); + + if (slideIndex >= slides.length || slideIndex < 0) { + console.error(`❌ Invalid slide index ${slideIndex}, max is ${slides.length - 1}`); + return; + } + + slides.forEach((slide, index) => { + slide.classList.remove('active', 'prev', 'next'); + + if (index === slideIndex) { + slide.classList.add('active'); + console.log(`βœ… Activated slide ${index}: ${slide.getAttribute('data-slide')} - Title: ${slide.querySelector('.beatport-rebuild-track-title')?.textContent}`); + } else if (index < slideIndex) { + slide.classList.add('prev'); + } else { + slide.classList.add('next'); + } + }); + + // Update indicators + const indicators = document.querySelectorAll('#genre-hero-slider .beatport-rebuild-indicator'); + indicators.forEach((indicator, index) => { + indicator.classList.toggle('active', index === slideIndex); + }); + + console.log(`Genre slide updated to: ${window.genreHeroSliderState.currentSlide}`); +} + +function startGenreHeroSliderAutoPlay() { + if (!window.genreHeroSliderState) { + console.error('❌ Cannot start auto-play: Genre hero slider state not initialized'); + return; + } + + // Clear any existing intervals first + if (window.genreHeroSliderState.autoPlayInterval) { + clearInterval(window.genreHeroSliderState.autoPlayInterval); + console.log('🧹 Cleared existing auto-play interval'); + } + + window.genreHeroSliderState.autoPlayInterval = setInterval(() => { + if (!window.genreHeroSliderState) { + console.error('❌ Auto-play fired but state is gone, clearing interval'); + clearInterval(window.genreHeroSliderState.autoPlayInterval); + return; + } + + const currentSlide = window.genreHeroSliderState.currentSlide; + const totalSlides = window.genreHeroSliderState.totalSlides; + const nextSlide = (currentSlide + 1) % totalSlides; + + console.log(`⏰ Auto-play: Current=${currentSlide}, Total=${totalSlides}, Next=${nextSlide}`); + + // Validate the next slide index + if (nextSlide >= 0 && nextSlide < totalSlides) { + updateGenreHeroSlide(nextSlide); + } else { + console.error(`❌ Invalid nextSlide calculated: ${nextSlide}, resetting to 0`); + updateGenreHeroSlide(0); + } + }, 5000); // 5 second intervals like the main slider + + console.log(`▢️ Started auto-play for genre hero slider (${window.genreHeroSliderState.totalSlides} slides)`); +} + // Initialize the Genre Browser Modal when the page loads document.addEventListener('DOMContentLoaded', () => { initializeGenreBrowserModal(); diff --git a/webui/static/style.css b/webui/static/style.css index d4911a54..063995e5 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -14632,7 +14632,7 @@ body { display: none; justify-content: center; align-items: center; - z-index: 9999; + z-index: 8888; animation: genreBrowserFadeIn 0.3s ease-out; } @@ -14927,3 +14927,146 @@ body { transform: rotate(360deg); } } + +/* === GENRE PAGE VIEW STYLES === */ + +.genre-page-content { + display: none; + width: 100%; + height: 100%; + animation: genreBrowserSlideIn 0.3s ease; +} + +.genre-page-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.genre-back-button { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: linear-gradient(135deg, + rgba(30, 30, 30, 0.8) 0%, + rgba(20, 20, 20, 0.9) 100%); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 10px; + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.genre-back-button:hover { + background: linear-gradient(135deg, + rgba(40, 40, 40, 0.9) 0%, + rgba(30, 30, 30, 0.95) 100%); + border-color: rgba(255, 255, 255, 0.25); + transform: translateX(-2px); +} + +.back-icon { + font-size: 16px; + transition: transform 0.2s ease; +} + +.genre-back-button:hover .back-icon { + transform: translateX(-2px); +} + +.genre-page-title { + font-size: 24px; + font-weight: 600; + color: #ffffff; + margin: 0; +} + +.genre-hero-slider-container { + width: 100%; + height: calc(100% - 80px); + overflow-y: auto; +} + +.genre-loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: rgba(255, 255, 255, 0.7); +} + +.genre-loading-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(255, 255, 255, 0.1); + border-top: 3px solid #1db954; + border-radius: 50%; + animation: genreBrowserSpin 1s linear infinite; + margin-bottom: 15px; +} + +.genre-loading-text { + font-size: 16px; + margin: 0; + text-align: center; +} + +.genre-error-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; +} + +.genre-error-text { + font-size: 18px; + color: #ff6b6b; + margin: 0 0 10px 0; +} + +.genre-error-details { + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + margin: 0 0 20px 0; +} + +.genre-retry-button { + padding: 10px 20px; + background: linear-gradient(135deg, + rgba(30, 30, 30, 0.8) 0%, + rgba(20, 20, 20, 0.9) 100%); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 10px; + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.genre-retry-button:hover { + background: linear-gradient(135deg, + rgba(40, 40, 40, 0.9) 0%, + rgba(30, 30, 30, 0.95) 100%); + border-color: rgba(255, 255, 255, 0.25); +} + +/* Responsive adjustments for genre page */ +@media (max-width: 768px) { + .genre-page-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .genre-page-title { + font-size: 20px; + } +}