Spaces:
Sleeping
Sleeping
// 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' ? | |
'<i class="fas fa-moon"></i>' : | |
'<i class="fas fa-sun"></i>'; | |
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' ? | |
'<i class="fas fa-moon"></i>' : | |
'<i class="fas fa-sun"></i>'; | |
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 = '<i class="fas fa-pause"></i>'; | |
} else { | |
audioElement.pause(); | |
isPlaying = false; | |
document.getElementById('play-pause').innerHTML = '<i class="fas fa-play"></i>'; | |
} | |
} | |
// 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 = ` | |
<div class="track-info"> | |
<span class="track-number">${index + 1}</span> | |
<div class="track-artwork"> | |
${metadata.artwork ? | |
`<img src="data:${metadata.artwork.mime};base64,${metadata.artwork.data}" alt="Album artwork">` : | |
'<i class="fas fa-music"></i>' | |
} | |
</div> | |
<div class="track-content"> | |
<div class="track-title"> | |
${title} | |
<span class="track-duration">${duration}</span> | |
</div> | |
<div class="track-metadata"> | |
${artist} | |
${metadata.album ? ` • ${metadata.album}` : ''} | |
</div> | |
</div> | |
</div> | |
`; | |
trackElement.addEventListener('click', () => playTrack(index)); | |
tracksList.appendChild(trackElement); | |
}); | |
} | |
// Update now playing information | |
function updateNowPlayingInfo() { | |
if (currentTrackIndex >= 0 && currentTrackIndex < playlist.length) { | |
const track = playlist[currentTrackIndex]; | |
const metadata = track.metadata || {}; | |
const nowPlayingTitle = document.querySelector('.now-playing-title'); | |
const nowPlayingArtist = document.querySelector('.now-playing-artist'); | |
const trackArtwork = document.querySelector('.now-playing-info .track-artwork'); | |
if (nowPlayingTitle) { | |
nowPlayingTitle.textContent = metadata.title || track.name; | |
} | |
if (nowPlayingArtist) { | |
let artistInfo = metadata.artist || 'Unknown Artist'; | |
if (metadata.album) { | |
artistInfo += ` • ${metadata.album}`; | |
} | |
nowPlayingArtist.textContent = artistInfo; | |
} | |
if (trackArtwork) { | |
trackArtwork.innerHTML = metadata.artwork ? | |
`<img src="data:${metadata.artwork.mime};base64,${metadata.artwork.data}" alt="Album artwork">` : | |
'<i class="fas fa-music"></i>'; | |
} | |
} | |
} | |
// Update playTrack function to properly set up time updates | |
async function playTrack(index) { | |
if (index < 0 || index >= playlist.length) return; | |
try { | |
// Stop current playback | |
if (audioElement) { | |
audioElement.pause(); | |
audioElement.currentTime = 0; | |
// Remove existing listeners | |
audioElement.removeEventListener('timeupdate', updateTimeDisplay); | |
audioElement.removeEventListener('loadedmetadata', updateTimeDisplay); | |
audioElement.removeEventListener('ended', handleTrackEnd); | |
} | |
currentTrackIndex = index; | |
const track = playlist[currentTrackIndex]; | |
if (!audioElement) { | |
console.warn('Audio element not initialized'); | |
return; | |
} | |
// Update source and load new track | |
audioElement.src = track.url; | |
await audioElement.load(); | |
// Add event listeners for time updates | |
audioElement.addEventListener('timeupdate', updateTimeDisplay); | |
audioElement.addEventListener('loadedmetadata', updateTimeDisplay); | |
audioElement.addEventListener('ended', handleTrackEnd); | |
// Update playlist UI | |
document.querySelectorAll('.playlist-item').forEach((item, i) => { | |
item.classList.toggle('active', i === index); | |
}); | |
// Force initial time display update | |
updateTimeDisplay(); | |
// Attempt to play with retry logic | |
try { | |
await audioElement.play(); | |
isPlaying = true; | |
updatePlayPauseButton(); | |
updateNowPlayingInfo(); | |
} catch (playError) { | |
console.warn('Play interrupted, retrying...', playError); | |
// Add a small delay before retrying | |
setTimeout(async () => { | |
try { | |
await audioElement.play(); | |
isPlaying = true; | |
updatePlayPauseButton(); | |
updateNowPlayingInfo(); | |
} catch (retryError) { | |
console.error('Failed to play after retry:', retryError); | |
showError('Failed to play track. Please try again.'); | |
} | |
}, 100); | |
} | |
} catch (error) { | |
console.error('Error playing track:', error); | |
showError('Error playing track'); | |
} | |
} | |
// Add these functions after the existing initialization code | |
function setupToggleHandlers() { | |
const mainContent = document.querySelector('.main-content'); | |
const uploadArea = document.querySelector('.upload-area'); | |
const playlistContainer = document.querySelector('.playlist-container'); | |
const uploadToggleBtn = document.querySelector('.upload-toggle-btn'); | |
const playlistToggleBtn = document.querySelector('.playlist-toggle-btn'); | |
const vizTypeBtn = document.querySelector('.viz-type-btn'); | |
const vizTypeDropdown = document.querySelector('.viz-type-dropdown'); | |
if (!mainContent || !uploadArea || !playlistContainer) { | |
console.error('Required elements not found'); | |
return; | |
} | |
// Show playlist by default | |
mainContent.classList.add('visible'); | |
playlistContainer.classList.add('visible'); | |
if (playlistToggleBtn) playlistToggleBtn.classList.add('active'); | |
// Upload button handler | |
uploadToggleBtn?.addEventListener('click', () => { | |
const isVisible = uploadArea.classList.contains('visible'); | |
// Hide playlist if it's visible | |
playlistContainer.classList.remove('visible'); | |
playlistToggleBtn?.classList.remove('active'); | |
// Toggle upload area | |
uploadArea.classList.toggle('visible'); | |
uploadToggleBtn.classList.toggle('active'); | |
// Show/hide main content | |
mainContent.classList.toggle('visible', !isVisible || playlistContainer.classList.contains('visible')); | |
}); | |
// Playlist button handler | |
playlistToggleBtn?.addEventListener('click', () => { | |
const isVisible = playlistContainer.classList.contains('visible'); | |
// Hide upload area if it's visible | |
uploadArea.classList.remove('visible'); | |
uploadToggleBtn?.classList.remove('active'); | |
// Toggle playlist | |
playlistContainer.classList.toggle('visible'); | |
playlistToggleBtn.classList.toggle('active'); | |
// Show/hide main content | |
mainContent.classList.toggle('visible', !isVisible || uploadArea.classList.contains('visible')); | |
}); | |
// Visualization type button handler | |
let isVizDropdownVisible = false; | |
vizTypeBtn?.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
isVizDropdownVisible = !isVizDropdownVisible; | |
if (isVizDropdownVisible) { | |
vizTypeDropdown?.classList.add('visible'); | |
vizTypeBtn.classList.add('active'); | |
} else { | |
vizTypeDropdown?.classList.remove('visible'); | |
vizTypeBtn.classList.remove('active'); | |
} | |
}); | |
// Handle visualization type selection | |
const vizTypeOptions = document.querySelectorAll('.viz-type-options button'); | |
vizTypeOptions?.forEach(button => { | |
button.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
const type = button.dataset.type; | |
// Update active state | |
vizTypeOptions.forEach(btn => btn.classList.remove('active')); | |
button.classList.add('active'); | |
// Update visualization | |
visualizationType = type; | |
createVisualization(); | |
// Close dropdown | |
isVizDropdownVisible = false; | |
vizTypeDropdown?.classList.remove('visible'); | |
vizTypeBtn?.classList.remove('active'); | |
}); | |
}); | |
// Close dropdowns when clicking outside | |
document.addEventListener('click', (e) => { | |
if (isVizDropdownVisible && | |
vizTypeDropdown && | |
!vizTypeDropdown.contains(e.target) && | |
!vizTypeBtn?.contains(e.target)) { | |
isVizDropdownVisible = false; | |
vizTypeDropdown.classList.remove('visible'); | |
vizTypeBtn?.classList.remove('active'); | |
} | |
}); | |
} | |
// Add updatePlayPauseButton function | |
function updatePlayPauseButton() { | |
const playPauseBtn = document.getElementById('play-pause'); | |
if (!playPauseBtn) return; | |
playPauseBtn.innerHTML = isPlaying ? | |
'<i class="fas fa-pause"></i>' : | |
'<i class="fas fa-play"></i>'; | |
// Update button state | |
playPauseBtn.disabled = !audioElement || !playlist.length; | |
} | |
// Setup shuffle and repeat buttons | |
function setupPlaylistControls() { | |
const shuffleBtn = document.querySelector('.shuffle-btn'); | |
const repeatBtn = document.querySelector('.repeat-btn'); | |
// Setup shuffle button | |
shuffleBtn?.addEventListener('click', () => { | |
isShuffleActive = !isShuffleActive; | |
shuffleBtn.classList.toggle('active', isShuffleActive); | |
if (isShuffleActive) { | |
// Save current track index | |
const currentTrack = playlist[currentTrackIndex]; | |
// Shuffle playlist | |
for (let i = playlist.length - 1; i > 0; i--) { | |
const j = Math.floor(Math.random() * (i + 1)); | |
[playlist[i], playlist[j]] = [playlist[j], playlist[i]]; | |
} | |
// Find new index of current track | |
currentTrackIndex = playlist.findIndex(track => track === currentTrack); | |
// Update playlist UI | |
createPlaylist(); | |
updateNowPlayingInfo(); | |
} else { | |
// Restore original order if needed | |
playlist.sort((a, b) => a.originalIndex - b.originalIndex); | |
createPlaylist(); | |
} | |
}); | |
// Setup repeat button | |
repeatBtn?.addEventListener('click', () => { | |
isRepeatActive = !isRepeatActive; | |
repeatBtn.classList.toggle('active', isRepeatActive); | |
}); | |
} | |
// Update handleFiles function for faster uploads | |
async function handleFiles(files) { | |
console.log('Handling files:', files.length, 'files'); | |
if (!files || files.length === 0) { | |
console.warn('No files selected'); | |
showError('No files selected'); | |
return; | |
} | |
if (!audioContext) { | |
try { | |
console.log('Initializing audio context...'); | |
initAudio(); | |
} catch (error) { | |
console.error('Failed to initialize audio:', error); | |
showError('Failed to initialize audio system'); | |
return; | |
} | |
} | |
const formData = new FormData(); | |
const totalSize = Array.from(files).reduce((acc, file) => acc + file.size, 0); | |
let uploadedSize = 0; | |
// Add files to FormData with optimized chunk size | |
Array.from(files).forEach(file => { | |
console.log('Adding file to upload:', file.name); | |
formData.append('files[]', file); | |
}); | |
// Show upload progress | |
const uploadProgress = document.querySelector('.upload-progress'); | |
const progressFill = uploadProgress?.querySelector('.progress-fill'); | |
const progressText = uploadProgress?.querySelector('.progress-text'); | |
if (uploadProgress && progressFill && progressText) { | |
uploadProgress.classList.add('visible'); | |
progressFill.style.width = '0%'; | |
progressText.textContent = '0%'; | |
} | |
try { | |
console.log('Starting file upload...'); | |
const response = await fetch('/upload', { | |
method: 'POST', | |
body: formData, | |
headers: sessionId ? { | |
'X-Session-ID': sessionId | |
} : {} | |
}); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
console.log('Upload response:', data); | |
if (data.success) { | |
console.log('Files uploaded successfully'); | |
showSuccess('Files uploaded successfully'); | |
const successfulFiles = data.files.filter(file => file.success); | |
console.log('Successful uploads:', successfulFiles.length, 'files'); | |
if (successfulFiles.length === 0) { | |
console.warn('No files were uploaded successfully'); | |
showError('No files were uploaded successfully'); | |
return; | |
} | |
// Get the next index for new tracks | |
const startIndex = playlist.length; | |
// Create new track objects | |
const newTracks = successfulFiles.map((file, index) => ({ | |
name: file.filename, | |
url: file.filepath, | |
metadata: file.metadata, | |
originalIndex: startIndex + index | |
})); | |
// Append new tracks to existing playlist | |
playlist = [...playlist, ...newTracks]; | |
console.log('Updated playlist:', playlist); | |
createPlaylist(); | |
// Only start playing if nothing is currently playing | |
if (playlist.length > 0 && !isPlaying) { | |
console.log('Playing first track...'); | |
playTrack(0); | |
} | |
// Enable player controls | |
document.querySelectorAll('.control-btn').forEach(btn => { | |
btn.disabled = false; | |
}); | |
// Highlight currently playing track if any | |
if (currentTrackIndex >= 0) { | |
document.querySelectorAll('.playlist-item').forEach((item, i) => { | |
item.classList.toggle('active', i === currentTrackIndex); | |
}); | |
} | |
// Store session ID if we got one | |
if (data.session_id) { | |
sessionId = data.session_id; | |
localStorage.setItem('audioSessionId', sessionId); | |
} | |
} else { | |
console.error('Upload failed:', data.error); | |
showError(data.error || 'Upload failed'); | |
} | |
} catch (error) { | |
console.error('Upload error:', error); | |
showError('Error uploading files'); | |
} finally { | |
if (uploadProgress) { | |
uploadProgress.classList.remove('visible'); | |
} | |
} | |
} | |
// Update volume control functions | |
function setupVolumeControl() { | |
const volumeBtn = document.querySelector('.volume-btn'); | |
const volumeSlider = document.getElementById('volume'); | |
const volumeProgress = document.querySelector('.volume-progress'); | |
const volumeHandle = document.querySelector('.volume-handle'); | |
if (!volumeBtn || !volumeSlider || !volumeProgress || !volumeHandle) { | |
console.error('Volume control elements not found'); | |
return; | |
} | |
// Initialize volume | |
let currentVolume = localStorage.getItem('volume') || 0.5; | |
updateVolume(currentVolume); | |
// Update volume on slider change | |
volumeSlider.addEventListener('input', (e) => { | |
const value = parseFloat(e.target.value); | |
updateVolume(value); | |
}); | |
// Update volume on button click (mute/unmute) | |
volumeBtn.addEventListener('click', () => { | |
if (audioElement) { | |
if (audioElement.volume > 0) { | |
// Store current volume before muting | |
localStorage.setItem('previousVolume', audioElement.volume); | |
updateVolume(0); | |
} else { | |
// Restore previous volume or default to 0.5 | |
const previousVolume = localStorage.getItem('previousVolume') || 0.5; | |
updateVolume(previousVolume); | |
} | |
} | |
}); | |
function updateVolume(value) { | |
// Update audio element | |
if (audioElement) { | |
audioElement.volume = value; | |
} | |
// Update UI | |
volumeProgress.style.width = `${value * 100}%`; | |
volumeHandle.style.left = `${value * 100}%`; | |
volumeSlider.value = value; | |
// Update button icon | |
const icon = volumeBtn.querySelector('i'); | |
if (icon) { | |
if (value === 0) { | |
icon.className = 'fas fa-volume-mute'; | |
} else if (value < 0.5) { | |
icon.className = 'fas fa-volume-down'; | |
} else { | |
icon.className = 'fas fa-volume-up'; | |
} | |
} | |
// Save volume to localStorage | |
localStorage.setItem('volume', value); | |
} | |
} | |
// Add to initialization | |
document.addEventListener('DOMContentLoaded', () => { | |
// ... other initialization code ... | |
setupVolumeControl(); | |
}); | |
// Add session management | |
let sessionId = localStorage.getItem('audioSessionId'); | |
// Add cleanup on page unload | |
window.addEventListener('beforeunload', async () => { | |
if (sessionId) { | |
try { | |
await fetch('/end-session', { | |
method: 'POST', | |
headers: { | |
'X-Session-ID': sessionId | |
} | |
}); | |
localStorage.removeItem('audioSessionId'); | |
} catch (error) { | |
console.error('Error ending session:', error); | |
} | |
} | |
}); |