feat: replace TTS modal audio embed with custom player controls

## Enhanced TTS Audio Experience:
- **Remove browser default audio controls** (was showing embed instead of custom player)
- **Add custom play/pause button** with emoji icons (⏸️ Pause / ▶️ Play / ▶️ Replay)
- **Add interactive progress bar** with click-to-seek functionality
- **Add time display** showing current time / total duration (e.g., "1:23 / 3:45")
- **Prominent download button** (💾 Download) for saving MP3 files
- **Auto-play with fallback** - attempts auto-play, gracefully handles browser restrictions

## UI Improvements:
- **Clean layout** with flexbox controls container
- **Visual progress indicator** with smooth transitions
- **Responsive design** supporting both custom and default styling modes
- **Proper spacing and styling** for professional appearance

## Functionality:
- **Click progress bar to seek** to any position in audio
- **Real-time progress updates** during playback
- **Proper state management** (play/pause/ended states)
- **Error handling** for audio playback issues
- **Accessibility** with proper button labels and time displays

## Technical Details:
- Hidden audio element (no browser controls)
- Custom event listeners for timeupdate, ended, error events
- Proper time formatting (MM:SS format)
- Progress calculation with percentage-based width updates
- Browser auto-play policy compliance with user interaction fallback

**Result: Professional audio player experience instead of basic browser embed\!** 🎵
This commit is contained in:
Russell Ballestrini 2025-07-03 19:01:37 -04:00
parent 03e402fdf9
commit ade7f4a35e

View file

@ -283,29 +283,203 @@ export function openTTSModal() {
resultDiv.style.display = "block";
resultDiv.innerHTML = "";
// Create hidden audio element (no controls)
const audio = new Audio(result.blobUrl);
audio.controls = true;
audio.style.width = "100%";
resultDiv.appendChild(audio);
audio.preload = "auto";
// Create custom control container
const controlsContainer = document.createElement("div");
if (USE_CUSTOM_STYLING) {
controlsContainer.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: white;
border-radius: 8px;
border: 1px solid #ddd;
margin-bottom: 12px;
`;
} else {
controlsContainer.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
margin-bottom: 12px;
`;
}
// Create play/pause button
const playPauseBtn = document.createElement("button");
playPauseBtn.textContent = "⏸️ Pause";
if (USE_CUSTOM_STYLING) {
playPauseBtn.style.cssText = `
padding: 8px 16px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
min-width: 100px;
`;
} else {
playPauseBtn.style.cssText = `
padding: 8px 16px;
min-width: 100px;
`;
}
// Create progress bar
const progressContainer = document.createElement("div");
if (USE_CUSTOM_STYLING) {
progressContainer.style.cssText = `
flex: 1;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
position: relative;
cursor: pointer;
`;
} else {
progressContainer.style.cssText = `
flex: 1;
height: 6px;
background: var(--border-color, #e0e0e0);
border-radius: 3px;
position: relative;
cursor: pointer;
`;
}
const progressBar = document.createElement("div");
if (USE_CUSTOM_STYLING) {
progressBar.style.cssText = `
height: 100%;
background: #2196F3;
border-radius: 3px;
width: 0%;
transition: width 0.1s;
`;
} else {
progressBar.style.cssText = `
height: 100%;
background: var(--accent-color, #2196F3);
border-radius: 3px;
width: 0%;
transition: width 0.1s;
`;
}
progressContainer.appendChild(progressBar);
// Create time display
const timeDisplay = document.createElement("span");
timeDisplay.textContent = "0:00 / 0:00";
if (USE_CUSTOM_STYLING) {
timeDisplay.style.cssText = `
font-size: 12px;
color: #666;
min-width: 80px;
text-align: center;
`;
}
// Create download button
const downloadBtn = document.createElement("button");
downloadBtn.textContent = "💾 Download Audio";
downloadBtn.textContent = "💾 Download";
if (USE_CUSTOM_STYLING) {
downloadBtn.style.cssText = `
margin-top: 8px;
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
`;
} else {
downloadBtn.style.cssText = `
padding: 8px 16px;
`;
}
downloadBtn.onclick = () => downloadAudio(result.blobUrl, "tts-audio.mp3");
resultDiv.appendChild(downloadBtn);
// Auto-play
audio.play();
// Assemble controls
controlsContainer.appendChild(playPauseBtn);
controlsContainer.appendChild(progressContainer);
controlsContainer.appendChild(timeDisplay);
controlsContainer.appendChild(downloadBtn);
resultDiv.appendChild(controlsContainer);
// Audio event handlers
let isPlaying = false;
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const updateProgress = () => {
if (audio.duration) {
const progress = (audio.currentTime / audio.duration) * 100;
progressBar.style.width = `${progress}%`;
timeDisplay.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`;
}
};
playPauseBtn.onclick = () => {
if (isPlaying) {
audio.pause();
playPauseBtn.textContent = "▶️ Play";
isPlaying = false;
} else {
audio.play();
playPauseBtn.textContent = "⏸️ Pause";
isPlaying = true;
}
};
// Progress bar click handler
progressContainer.onclick = (e) => {
if (audio.duration) {
const rect = progressContainer.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickRatio = clickX / rect.width;
audio.currentTime = clickRatio * audio.duration;
}
};
// Audio event listeners
audio.addEventListener('loadedmetadata', () => {
timeDisplay.textContent = `0:00 / ${formatTime(audio.duration)}`;
});
audio.addEventListener('timeupdate', updateProgress);
audio.addEventListener('ended', () => {
playPauseBtn.textContent = "▶️ Replay";
isPlaying = false;
progressBar.style.width = '0%';
audio.currentTime = 0;
});
audio.addEventListener('error', (e) => {
console.error('Audio playback error:', e);
playPauseBtn.textContent = "❌ Error";
playPauseBtn.disabled = true;
});
// Auto-play and set initial state
audio.play().then(() => {
isPlaying = true;
playPauseBtn.textContent = "⏸️ Pause";
}).catch((e) => {
console.warn('Auto-play failed, user interaction required:', e);
isPlaying = false;
playPauseBtn.textContent = "▶️ Play";
});
}
} catch (error) {
alert(getUIText("ttsFailed", { error: error.message }));