|
class WavePlayer { |
|
constructor(container, options = {}) { |
|
this.container = container; |
|
this.options = { |
|
waveColor: '#d1d6e0', |
|
progressColor: '#5046e5', |
|
cursorColor: '#5046e5', |
|
cursorWidth: 2, |
|
height: 80, |
|
responsive: true, |
|
barWidth: 2, |
|
barGap: 1, |
|
hideScrollbar: true, |
|
...options |
|
}; |
|
|
|
this.isPlaying = false; |
|
this.wavesurfer = null; |
|
this.loadingIndicator = null; |
|
this.playButton = null; |
|
|
|
this.init(); |
|
} |
|
|
|
init() { |
|
|
|
this.buildUI(); |
|
|
|
|
|
this.initWavesurfer(); |
|
|
|
|
|
this.setupEvents(); |
|
} |
|
|
|
buildUI() { |
|
|
|
this.container.innerHTML = ''; |
|
this.container.classList.add('waveplayer'); |
|
|
|
|
|
const style = document.createElement('style'); |
|
style.textContent = ` |
|
.waveplayer audio { |
|
display: none !important; |
|
} |
|
|
|
/* Mobile optimizations */ |
|
@media (max-width: 768px) { |
|
.waveplayer-play-btn { |
|
width: 44px; |
|
height: 44px; |
|
margin-right: 12px; |
|
} |
|
|
|
.waveplayer-waveform { |
|
height: 70px; |
|
cursor: pointer; |
|
touch-action: none; /* Prevents scroll/zoom on touch */ |
|
} |
|
} |
|
`; |
|
this.container.appendChild(style); |
|
|
|
|
|
const waveformContainer = document.createElement('div'); |
|
waveformContainer.className = 'waveplayer-waveform'; |
|
|
|
const controlsContainer = document.createElement('div'); |
|
controlsContainer.className = 'waveplayer-controls'; |
|
|
|
|
|
this.playButton = document.createElement('button'); |
|
this.playButton.className = 'waveplayer-play-btn'; |
|
this.playButton.innerHTML = ` |
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="play-icon"> |
|
<polygon points="5 3 19 12 5 21 5 3"></polygon> |
|
</svg> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pause-icon" style="display: none;"> |
|
<rect x="6" y="4" width="4" height="16"></rect> |
|
<rect x="14" y="4" width="4" height="16"></rect> |
|
</svg> |
|
`; |
|
|
|
|
|
this.timeDisplay = document.createElement('div'); |
|
this.timeDisplay.className = 'waveplayer-time'; |
|
this.timeDisplay.textContent = '0:00 / 0:00'; |
|
|
|
|
|
this.loadingIndicator = document.createElement('div'); |
|
this.loadingIndicator.className = 'waveplayer-loading'; |
|
this.loadingIndicator.innerHTML = ` |
|
<div class="waveplayer-spinner"></div> |
|
<span>Loading...</span> |
|
`; |
|
|
|
|
|
const loadingTextElement = this.loadingIndicator.querySelector('span'); |
|
if (loadingTextElement) { |
|
const observer = new MutationObserver((mutations) => { |
|
mutations.forEach((mutation) => { |
|
if (mutation.type === 'characterData' || mutation.type === 'childList') { |
|
const text = loadingTextElement.textContent; |
|
if (text && text.includes('100%')) { |
|
|
|
setTimeout(() => this.hideLoading(), 300); |
|
} |
|
} |
|
}); |
|
}); |
|
|
|
observer.observe(loadingTextElement, { |
|
characterData: true, |
|
childList: true, |
|
subtree: true |
|
}); |
|
} |
|
|
|
|
|
controlsContainer.appendChild(this.playButton); |
|
controlsContainer.appendChild(this.timeDisplay); |
|
|
|
this.container.appendChild(controlsContainer); |
|
this.container.appendChild(waveformContainer); |
|
this.container.appendChild(this.loadingIndicator); |
|
|
|
|
|
this.waveformContainer = waveformContainer; |
|
} |
|
|
|
initWavesurfer() { |
|
|
|
this.wavesurfer = WaveSurfer.create({ |
|
container: this.waveformContainer, |
|
...this.options, |
|
|
|
interact: true, |
|
dragToSeek: true |
|
}); |
|
|
|
|
|
if (this.loadingIndicator) { |
|
this.loadingIndicator.style.display = 'none'; |
|
} |
|
} |
|
|
|
setupEvents() { |
|
|
|
this.playButton.addEventListener('click', () => { |
|
this.togglePlayPause(); |
|
}); |
|
|
|
|
|
this.playButton.addEventListener('touchstart', (e) => { |
|
e.preventDefault(); |
|
this.togglePlayPause(); |
|
}); |
|
|
|
|
|
this.waveformContainer.addEventListener('touchstart', (e) => { |
|
|
|
e.stopPropagation(); |
|
}); |
|
|
|
|
|
this.wavesurfer.on('ready', () => { |
|
|
|
if (this.loadingTimeout) { |
|
clearTimeout(this.loadingTimeout); |
|
} |
|
|
|
|
|
this.hideLoading(); |
|
this.updateTimeDisplay(); |
|
|
|
|
|
if (this.loadingIndicator && this.loadingIndicator.querySelector('span')) { |
|
this.loadingIndicator.querySelector('span').textContent = 'Loading...'; |
|
} |
|
|
|
console.log('WavePlayer ready event fired'); |
|
}); |
|
|
|
|
|
this.wavesurfer.on('decode', () => { |
|
|
|
this.hideLoading(); |
|
console.log('WavePlayer decode event fired'); |
|
}); |
|
|
|
|
|
this.wavesurfer.on('loading', (percent) => { |
|
this.showLoading(percent); |
|
|
|
|
|
if (percent === 100) { |
|
setTimeout(() => { |
|
this.hideLoading(); |
|
console.log('WavePlayer loading 100% - force hiding loader'); |
|
}, 500); |
|
} |
|
}); |
|
|
|
this.wavesurfer.on('play', () => { |
|
this.isPlaying = true; |
|
this.updatePlayButton(); |
|
}); |
|
|
|
this.wavesurfer.on('pause', () => { |
|
this.isPlaying = false; |
|
this.updatePlayButton(); |
|
}); |
|
|
|
this.wavesurfer.on('finish', () => { |
|
this.isPlaying = false; |
|
this.updatePlayButton(); |
|
}); |
|
|
|
this.wavesurfer.on('audioprocess', () => { |
|
this.updateTimeDisplay(); |
|
}); |
|
|
|
this.wavesurfer.on('seek', () => { |
|
this.updateTimeDisplay(); |
|
}); |
|
|
|
this.wavesurfer.on('error', (err) => { |
|
console.error('WaveSurfer error:', err); |
|
this.hideLoading(); |
|
}); |
|
} |
|
|
|
loadAudio(url) { |
|
this.showLoading(); |
|
this.wavesurfer.load(url); |
|
|
|
|
|
|
|
this.loadingTimeout = setTimeout(() => { |
|
this.hideLoading(); |
|
}, 10000); |
|
} |
|
|
|
play() { |
|
this.wavesurfer.play(); |
|
} |
|
|
|
pause() { |
|
this.wavesurfer.pause(); |
|
} |
|
|
|
togglePlayPause() { |
|
this.wavesurfer.playPause(); |
|
} |
|
|
|
stop() { |
|
this.wavesurfer.stop(); |
|
} |
|
|
|
updatePlayButton() { |
|
const playIcon = this.playButton.querySelector('.play-icon'); |
|
const pauseIcon = this.playButton.querySelector('.pause-icon'); |
|
|
|
if (this.isPlaying) { |
|
playIcon.style.display = 'none'; |
|
pauseIcon.style.display = 'block'; |
|
} else { |
|
playIcon.style.display = 'block'; |
|
pauseIcon.style.display = 'none'; |
|
} |
|
} |
|
|
|
showLoading(percent) { |
|
this.loadingIndicator.style.display = 'flex'; |
|
if (percent !== undefined) { |
|
this.loadingIndicator.querySelector('span').textContent = `Loading: ${Math.round(percent)}%`; |
|
} |
|
} |
|
|
|
hideLoading() { |
|
if (this.loadingIndicator) { |
|
this.loadingIndicator.style.display = 'none'; |
|
|
|
|
|
const loadingText = this.loadingIndicator.querySelector('span'); |
|
if (loadingText) { |
|
loadingText.textContent = 'Loading...'; |
|
} |
|
} |
|
} |
|
|
|
formatTime(seconds) { |
|
const minutes = Math.floor(seconds / 60); |
|
const secondsRemainder = Math.round(seconds) % 60; |
|
const paddedSeconds = secondsRemainder.toString().padStart(2, '0'); |
|
return `${minutes}:${paddedSeconds}`; |
|
} |
|
|
|
updateTimeDisplay() { |
|
if (!this.wavesurfer.isReady) return; |
|
|
|
const currentTime = this.formatTime(this.wavesurfer.getCurrentTime()); |
|
const duration = this.formatTime(this.wavesurfer.getDuration()); |
|
this.timeDisplay.textContent = `${currentTime} / ${duration}`; |
|
} |
|
} |
|
|
|
|
|
window.WavePlayer = WavePlayer; |