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:
parent
03e402fdf9
commit
ade7f4a35e
1 changed files with 189 additions and 15 deletions
204
src/tts-modal.js
204
src/tts-modal.js
|
|
@ -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 }));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue