// Initialize Three.js scene and renderer const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Set initial background color based on theme const savedTheme = localStorage.getItem('theme') || 'dark'; document.documentElement.setAttribute('data-theme', savedTheme); renderer.setClearColor(savedTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.9); document.body.appendChild(renderer.domElement); // Add fog to the scene for depth scene.fog = new THREE.FogExp2(savedTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.02); // Initialize controls with enhanced settings const controls = new THREE.OrbitControls(camera, renderer.domElement); setupOrbitControls(); // Initialize other variables let audioContext; let analyser; let audioElement; let playlist = []; let currentTrackIndex = 0; let isPlaying = false; let visualizationType = 'sphere'; let visualizers = { bars: [], sphere: null, particles: null }; let isShuffleActive = false; let isRepeatActive = false; // Add visualization-specific camera positions const visualizerSettings = { bars: { cameraZ: 8, baseRadius: 2 }, sphere: { cameraZ: 5, baseRadius: 1 }, particles: { cameraZ: 20, baseRadius: 8 } }; // Theme button functionality const themeBtn = document.querySelector('.theme-btn'); if (themeBtn) { themeBtn.innerHTML = savedTheme === 'light' ? '' : ''; themeBtn.addEventListener('click', () => { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); themeBtn.innerHTML = newTheme === 'light' ? '' : ''; renderer.setClearColor(newTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.9); }); } // Initialize audio context function initAudio() { audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioElement = new Audio(); const source = audioContext.createMediaElementSource(audioElement); analyser = audioContext.createAnalyser(); analyser.fftSize = 2048; source.connect(analyser); analyser.connect(audioContext.destination); } // Visualization creation functions function createBarsVisualization() { const numBars = 180; const geometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 8); geometry.translate(0, 0.5, 0); // Move pivot to bottom // Create custom shader material for bars const material = new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, color1: { value: new THREE.Color(0x4CAF50) }, color2: { value: new THREE.Color(0x2196F3) }, color3: { value: new THREE.Color(0xFF4081) } }, vertexShader: ` varying vec3 vPosition; varying vec3 vNormal; void main() { vPosition = position; vNormal = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float time; uniform vec3 color1; uniform vec3 color2; uniform vec3 color3; varying vec3 vPosition; varying vec3 vNormal; void main() { float heightFactor = vPosition.y; // Create dynamic color gradient vec3 baseColor = mix( mix(color1, color2, heightFactor), color3, sin(time * 0.5) * 0.5 + 0.5 ); // Add fresnel effect for edge glow float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0, 0, 1.0))), 2.0); vec3 finalColor = mix(baseColor, vec3(1.0), fresnel * 0.5); gl_FragColor = vec4(finalColor, 0.9); } `, transparent: true, side: THREE.DoubleSide }); const radius = 4; const angleStep = (Math.PI * 2) / numBars; for (let i = 0; i < numBars; i++) { const angle = i * angleStep; const bar = new THREE.Mesh(geometry, material.clone()); // Position in a circle bar.position.x = Math.cos(angle) * radius; bar.position.z = Math.sin(angle) * radius; // Rotate to face center bar.rotation.y = -angle; // Store initial properties bar.userData.initialY = bar.position.y; bar.userData.initialScale = 1; bar.userData.angle = angle; bar.userData.index = i; scene.add(bar); visualizers.bars.push(bar); } // Add ambient light const ambientLight = new THREE.AmbientLight(0x404040, 0.5); scene.add(ambientLight); // Add multiple point lights with different colors const colors = [0xFF4081, 0x2196F3, 0x4CAF50]; colors.forEach((color, i) => { const light = new THREE.PointLight(color, 1, 20); const angle = (i / colors.length) * Math.PI * 2; const lightRadius = radius * 1.5; light.position.set( Math.cos(angle) * lightRadius, 5, Math.sin(angle) * lightRadius ); scene.add(light); }); } function createSphereVisualization() { const geometry = new THREE.IcosahedronGeometry(1, 4); // Create a more complex material with gradient and glow effects const material = new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, color1: { value: new THREE.Color(0x4CAF50) }, color2: { value: new THREE.Color(0x2196F3) }, color3: { value: new THREE.Color(0xFF4081) } }, vertexShader: ` varying vec3 vNormal; varying vec3 vPosition; void main() { vNormal = normalize(normalMatrix * normal); vPosition = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float time; uniform vec3 color1; uniform vec3 color2; uniform vec3 color3; varying vec3 vNormal; varying vec3 vPosition; void main() { float noise = sin(vPosition.x * 10.0 + time) * cos(vPosition.y * 10.0 + time) * sin(vPosition.z * 10.0 + time); vec3 color = mix( mix(color1, color2, noise * 0.5 + 0.5), color3, sin(time * 0.5) * 0.5 + 0.5 ); float fresnel = pow(1.0 + dot(vNormal, vec3(0, 0, 1.0)), 3.0); color = mix(color, vec3(1.0), fresnel * 0.7); gl_FragColor = vec4(color, 0.9); } `, transparent: true, side: THREE.DoubleSide }); visualizers.sphere = new THREE.Mesh(geometry, material); // Store original vertex positions visualizers.sphere.userData.originalPositions = geometry.attributes.position.array.slice(); scene.add(visualizers.sphere); // Add ambient light for base illumination const ambientLight = new THREE.AmbientLight(0x404040, 0.5); scene.add(ambientLight); // Add multiple point lights with different colors const colors = [0xFF4081, 0x2196F3, 0x4CAF50]; const radius = 5; colors.forEach((color, i) => { const light = new THREE.PointLight(color, 1, 20); const angle = (i / colors.length) * Math.PI * 2; light.position.set( Math.cos(angle) * radius, Math.sin(angle) * radius, radius ); scene.add(light); }); } function createParticlesVisualization() { const particleCount = 5000; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(particleCount * 3); const scales = new Float32Array(particleCount); const colors = new Float32Array(particleCount * 3); const color1 = new THREE.Color(0x4CAF50); const color2 = new THREE.Color(0x2196F3); const color3 = new THREE.Color(0xFF4081); // Create a spiral galaxy formation for (let i = 0; i < particleCount; i++) { const i3 = i * 3; const radius = (Math.random() * 3) + 2; const spinAngle = (i / particleCount) * Math.PI * 24; const heightRange = Math.random() * Math.PI * 2; // Create spiral arms positions[i3] = Math.cos(spinAngle + radius) * radius; positions[i3 + 1] = Math.sin(heightRange) * (radius * 0.2); positions[i3 + 2] = Math.sin(spinAngle + radius) * radius; // Vary particle sizes scales[i] = Math.random() * 0.5 + 0.5; // Create color gradient along the spiral const colorMix = Math.abs(Math.sin(spinAngle)); const finalColor = new THREE.Color().lerpColors( color1, colorMix > 0.5 ? color2 : color3, colorMix ); colors[i3] = finalColor.r; colors[i3 + 1] = finalColor.g; colors[i3 + 2] = finalColor.b; } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); // Create custom shader material for particles const material = new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, size: { value: 15.0 }, pixelRatio: { value: window.devicePixelRatio } }, vertexShader: ` attribute float scale; attribute vec3 color; uniform float time; uniform float size; uniform float pixelRatio; varying vec3 vColor; void main() { vColor = color; vec3 pos = position; // Add some movement float angle = time * 0.2; pos.x = position.x * cos(angle) - position.z * sin(angle); pos.z = position.x * sin(angle) + position.z * cos(angle); pos.y += sin(time + position.x * 0.5) * 0.3; vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); gl_Position = projectionMatrix * mvPosition; // Size attenuation gl_PointSize = size * scale * pixelRatio * (1.0 / -mvPosition.z); } `, fragmentShader: ` varying vec3 vColor; void main() { // Create circular particles vec2 xy = gl_PointCoord.xy - vec2(0.5); float radius = length(xy); float alpha = 1.0 - smoothstep(0.45, 0.5, radius); // Add glow effect vec3 glow = vColor * (1.0 - radius * 2.0); vec3 finalColor = mix(vColor, glow, 0.5); gl_FragColor = vec4(finalColor, alpha); } `, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }); visualizers.particles = new THREE.Points(geometry, material); scene.add(visualizers.particles); } // Visualization update functions function updateBarsVisualization(dataArray) { const time = Date.now() * 0.001; const multiplier = 0.02; visualizers.bars.forEach((bar, i) => { const value = dataArray[i % dataArray.length] * multiplier; // Update shader uniforms bar.material.uniforms.time.value = time; // Calculate dynamic height const baseHeight = value + 0.1; const wave = Math.sin(time * 2 + bar.userData.angle) * 0.1; const finalHeight = baseHeight + wave; // Update bar scale and position bar.scale.y = finalHeight; // Add floating effect bar.position.y = Math.sin(time + bar.userData.angle) * 0.1; // Add subtle rotation bar.rotation.x = Math.sin(time * 0.5 + bar.userData.angle) * 0.1; bar.rotation.z = Math.cos(time * 0.5 + bar.userData.angle) * 0.1; }); } function updateSphereVisualization(dataArray) { if (!visualizers.sphere) return; const positions = visualizers.sphere.geometry.attributes.position.array; const originalPositions = visualizers.sphere.userData.originalPositions; const time = Date.now() * 0.001; // Update shader uniforms visualizers.sphere.material.uniforms.time.value = time; // Create more complex deformation based on audio data for (let i = 0; i < positions.length; i += 3) { const i3 = i / 3; const value = dataArray[i3 % dataArray.length] / 255; const deform = value * 0.5; const noise = Math.sin(time + i3 * 0.1) * 0.2; positions[i] = originalPositions[i] * (1 + deform * Math.sin(time + i3) + noise); positions[i + 1] = originalPositions[i + 1] * (1 + deform * Math.cos(time + i3) + noise); positions[i + 2] = originalPositions[i + 2] * (1 + deform * Math.sin(time * 0.5 + i3) + noise); } visualizers.sphere.geometry.attributes.position.needsUpdate = true; // Add smooth rotation visualizers.sphere.rotation.y += 0.002; visualizers.sphere.rotation.x += 0.001; } function updateParticlesVisualization(dataArray) { if (!visualizers.particles) return; const time = Date.now() * 0.001; const positions = visualizers.particles.geometry.attributes.position.array; const scales = visualizers.particles.geometry.attributes.scale.array; const colors = visualizers.particles.geometry.attributes.color.array; // Update shader uniforms visualizers.particles.material.uniforms.time.value = time; for (let i = 0; i < positions.length; i += 3) { const i3 = i / 3; const value = dataArray[i3 % dataArray.length] / 255; // Update particle scales based on audio scales[i3] = (value * 0.5 + 0.5) * (Math.sin(time + i3) * 0.2 + 0.8); // Update colors with audio reactivity const hue = (i3 / positions.length) + time * 0.1; const saturation = 0.7 + value * 0.3; const lightness = 0.4 + value * 0.2; const color = new THREE.Color().setHSL(hue, saturation, lightness); colors[i] = color.r; colors[i + 1] = color.g; colors[i + 2] = color.b; } visualizers.particles.geometry.attributes.scale.needsUpdate = true; visualizers.particles.geometry.attributes.color.needsUpdate = true; } // Animation loop function animate() { requestAnimationFrame(animate); controls.update(); if (analyser && isPlaying) { const dataArray = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(dataArray); updateVisualization(dataArray); } renderer.render(scene, camera); } // Start animation animate(); // Event listeners for window resize window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // Initialize the application document.addEventListener('DOMContentLoaded', () => { // Initialize loading screen const loadingScreen = document.querySelector('.loading-screen'); function showLoading() { if (loadingScreen) { loadingScreen.classList.remove('hidden'); } } function hideLoading() { if (loadingScreen) { loadingScreen.classList.add('hidden'); } } // Show loading screen immediately showLoading(); // Initialize all components setupUploadHandlers(); setupPlayerControls(); setupToggleHandlers(); setupProgressBar(); setupPlaylistControls(); createVisualization(); // Hide loading screen after initialization hideLoading(); }); // Export necessary functions and variables window.playTrack = playTrack; window.createPlaylist = createPlaylist; window.updateNowPlayingInfo = updateNowPlayingInfo; // Setup orbit controls function setupOrbitControls() { controls.enableDamping = true; controls.dampingFactor = 0.05; controls.enableZoom = true; controls.autoRotate = true; controls.autoRotateSpeed = 1.5; // Camera position is now set in createVisualization } // Enhanced file upload handling function setupUploadHandlers() { console.log('Setting up upload handlers...'); const uploadArea = document.getElementById('upload-area'); const fileInput = document.getElementById('audio-upload'); const uploadProgress = document.querySelector('.upload-progress'); const progressFill = uploadProgress?.querySelector('.progress-fill'); const progressText = uploadProgress?.querySelector('.progress-text'); const errorToast = document.querySelector('.error-toast'); if (!uploadArea || !fileInput) { console.error('Upload elements not found'); return; } // Add logging for drag and drop events uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); console.log('File being dragged over upload area'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); console.log('File drag left upload area'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); console.log('Files dropped:', e.dataTransfer.files.length, 'files'); handleFiles(e.dataTransfer.files); }); uploadArea.addEventListener('click', () => { console.log('Upload area clicked, triggering file input'); fileInput.click(); }); fileInput.addEventListener('change', (e) => { console.log('Files selected:', e.target.files.length, 'files'); handleFiles(e.target.files); }); } // Enhanced toast notifications with null checks function showError(message) { const toast = document.querySelector('.error-toast'); if (!toast) return; toast.textContent = message; toast.className = 'error-toast error visible'; setTimeout(() => { toast.classList.remove('visible'); }, 3000); } function showSuccess(message) { const toast = document.querySelector('.error-toast'); if (!toast) return; toast.textContent = message; toast.className = 'error-toast success visible'; setTimeout(() => { toast.classList.remove('visible'); }, 3000); } // Enhanced keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.code === 'Space' && !e.target.matches('input, textarea')) { e.preventDefault(); togglePlayPause(); } else if (e.code === 'ArrowLeft') { playPrevious(); } else if (e.code === 'ArrowRight') { playNext(); } else if (e.code === 'KeyM') { toggleMute(); } }); // Volume control function toggleMute() { if (!audioElement) return; const volumeBtn = document.querySelector('.volume-btn i'); if (audioElement.volume > 0) { audioElement.volume = 0; volumeBtn.className = 'fas fa-volume-mute'; } else { audioElement.volume = 0.5; volumeBtn.className = 'fas fa-volume-up'; } updateVolumeUI(); } function updateVolumeUI() { const volumeProgress = document.querySelector('.volume-progress'); const volumeHandle = document.querySelector('.volume-handle'); const volumeSlider = document.getElementById('volume'); if (volumeProgress && volumeHandle && volumeSlider) { const value = audioElement ? audioElement.volume : 0.5; volumeProgress.style.width = `${value * 100}%`; volumeHandle.style.left = `${value * 100}%`; volumeSlider.value = value; } } // Enhanced progress bar interaction function setupProgressBar() { const progressContainer = document.querySelector('.progress-container'); const progressBar = document.querySelector('.progress-bar'); const progress = document.querySelector('.progress'); const progressHandle = document.querySelector('.progress-handle'); const seekSlider = document.querySelector('.seek-slider'); // Return early if required elements are not found if (!progressBar || !progress) { console.error('Progress bar elements not found'); return; } let isDragging = false; // Mouse events for desktop progressBar.addEventListener('mousedown', startDragging); document.addEventListener('mousemove', updateDragging); document.addEventListener('mouseup', stopDragging); // Touch events for mobile progressBar.addEventListener('touchstart', handleTouchStart); document.addEventListener('touchmove', handleTouchMove); document.addEventListener('touchend', handleTouchEnd); function handleTouchStart(e) { e.preventDefault(); isDragging = true; progressBar.classList.add('dragging'); updateProgress(e.touches[0]); } function handleTouchMove(e) { if (!isDragging) return; e.preventDefault(); updateProgress(e.touches[0]); } function handleTouchEnd() { isDragging = false; progressBar.classList.remove('dragging'); } function startDragging(e) { isDragging = true; progressBar.classList.add('dragging'); updateProgress(e); } function updateDragging(e) { if (!isDragging) return; updateProgress(e); } function stopDragging() { isDragging = false; progressBar.classList.remove('dragging'); } function updateProgress(e) { if (!audioElement || !audioElement.duration) return; try { const rect = progressBar.getBoundingClientRect(); const x = e.clientX || e.pageX; const percent = Math.min(Math.max((x - rect.left) / rect.width, 0), 1); // Update progress bar and handle progress.style.width = `${percent * 100}%`; if (progressHandle) { progressHandle.style.left = `${percent * 100}%`; } if (seekSlider) { seekSlider.value = percent * 100; } // Update audio time audioElement.currentTime = percent * audioElement.duration; // Force time display update updateTimeDisplay(); } catch (error) { console.error('Error updating progress:', error); } } // Add seek slider input handler if (seekSlider) { seekSlider.addEventListener('input', (e) => { if (!audioElement || !audioElement.duration) return; const percent = e.target.value; progress.style.width = `${percent}%`; if (progressHandle) { progressHandle.style.left = `${percent}%`; } audioElement.currentTime = (percent / 100) * audioElement.duration; updateTimeDisplay(); }); } } // Setup player controls function setupPlayerControls() { const playPauseBtn = document.getElementById('play-pause'); const prevBtn = document.querySelector('.previous-btn'); const nextBtn = document.querySelector('.next-btn'); const volumeSlider = document.getElementById('volume'); const seekSlider = document.querySelector('.seek-slider'); playPauseBtn.addEventListener('click', togglePlayPause); prevBtn.addEventListener('click', playPrevious); nextBtn.addEventListener('click', playNext); volumeSlider.addEventListener('input', updateVolume); // Add both input and change events for the seek slider seekSlider.addEventListener('input', seekTo); seekSlider.addEventListener('change', seekTo); // Add keyboard controls document.addEventListener('keydown', (e) => { if (e.code === 'Space') { e.preventDefault(); togglePlayPause(); } else if (e.code === 'ArrowLeft') { playPrevious(); } else if (e.code === 'ArrowRight') { playNext(); } }); // Update time display more frequently if (audioElement) { // Remove existing listeners first audioElement.removeEventListener('timeupdate', updateTimeDisplay); audioElement.removeEventListener('loadedmetadata', updateTimeDisplay); audioElement.removeEventListener('ended', handleTrackEnd); // Add listeners audioElement.addEventListener('timeupdate', updateTimeDisplay); audioElement.addEventListener('loadedmetadata', updateTimeDisplay); audioElement.addEventListener('ended', handleTrackEnd); // Force initial update updateTimeDisplay(); } } // Toggle play/pause function togglePlayPause() { if (!audioElement) return; if (audioElement.paused) { audioElement.play(); isPlaying = true; document.getElementById('play-pause').innerHTML = ''; } else { audioElement.pause(); isPlaying = false; document.getElementById('play-pause').innerHTML = ''; } } // Play previous track function playPrevious() { if (playlist.length === 0) return; let newIndex = currentTrackIndex - 1; if (newIndex < 0) newIndex = playlist.length - 1; playTrack(newIndex); } // Play next track function playNext() { if (playlist.length === 0) return; let newIndex = currentTrackIndex + 1; if (newIndex >= playlist.length) newIndex = 0; playTrack(newIndex); } // Update volume function updateVolume(e) { if (audioElement) { audioElement.volume = e.target.value; const volumeProgress = document.querySelector('.volume-progress'); volumeProgress.style.width = `${e.target.value * 100}%`; } } // Seek to position function seekTo(e) { if (!audioElement || !audioElement.duration) return; const seekSlider = e.target; const progress = document.querySelector('.progress'); const progressHandle = document.querySelector('.progress-handle'); const time = (seekSlider.value / 100) * audioElement.duration; // Update audio time audioElement.currentTime = time; // Update progress bar and handle if (progress) { progress.style.width = `${seekSlider.value}%`; } if (progressHandle) { progressHandle.style.left = `${seekSlider.value}%`; } // Force time display update updateTimeDisplay(); } // Update time display function updateTimeDisplay() { if (!audioElement) return; const currentTimeEl = document.querySelector('.current-time'); const totalTimeEl = document.querySelector('.total-time'); const progress = document.querySelector('.progress'); const progressHandle = document.querySelector('.progress-handle'); const seekSlider = document.querySelector('.seek-slider'); // Only update if duration is available and not NaN if (audioElement.duration && !isNaN(audioElement.duration)) { const current = formatTime(audioElement.currentTime); const total = formatTime(audioElement.duration); const progressPercent = (audioElement.currentTime / audioElement.duration) * 100; // Update time displays if (currentTimeEl) currentTimeEl.textContent = current; if (totalTimeEl) totalTimeEl.textContent = total; // Update progress bar and handle if (progress) { progress.style.width = `${progressPercent}%`; } if (progressHandle) { progressHandle.style.left = `${progressPercent}%`; } if (seekSlider && !seekSlider.matches(':active')) { seekSlider.value = progressPercent; } } else { // Reset displays if no duration available if (currentTimeEl) currentTimeEl.textContent = '0:00'; if (totalTimeEl) totalTimeEl.textContent = '0:00'; if (progress) progress.style.width = '0%'; if (progressHandle) progressHandle.style.left = '0%'; if (seekSlider) seekSlider.value = 0; } } // Format time helper function function formatTime(seconds) { if (!seconds || isNaN(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } // Handle track end function handleTrackEnd() { if (isRepeatActive) { audioElement.currentTime = 0; audioElement.play(); } else { playNext(); } } // Create visualization function createVisualization() { // Clear existing visualization while(scene.children.length > 0) { scene.remove(scene.children[0]); } visualizers = { bars: [], sphere: null, particles: null }; // Set camera position based on visualization type camera.position.z = visualizerSettings[visualizationType].cameraZ; switch(visualizationType) { case 'bars': createBarsVisualization(); break; case 'sphere': createSphereVisualization(); break; case 'particles': createParticlesVisualization(); break; } } // Update visualization function updateVisualization(dataArray) { switch(visualizationType) { case 'bars': updateBarsVisualization(dataArray); break; case 'sphere': updateSphereVisualization(dataArray); break; case 'particles': updateParticlesVisualization(dataArray); break; } } // Create playlist UI function createPlaylist() { const tracksList = document.querySelector('.tracks-list'); const noTracksMessage = document.querySelector('.no-tracks-message'); // Clear existing tracks tracksList.innerHTML = ''; if (!playlist || playlist.length === 0) { // Show no tracks message if playlist is empty if (noTracksMessage) { noTracksMessage.style.display = 'flex'; } return; } // Hide no tracks message if we have tracks if (noTracksMessage) { noTracksMessage.style.display = 'none'; } // Create track elements playlist.forEach((track, index) => { const metadata = track.metadata || {}; const duration = metadata.duration ? formatTime(metadata.duration) : '0:00'; const artist = metadata.artist || 'Unknown Artist'; const title = metadata.title || track.name; const trackElement = document.createElement('div'); trackElement.className = 'playlist-item'; trackElement.innerHTML = `