From ade7f4a35e78a4bfbe78b8d4d572d8340edd5ab8 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 3 Jul 2025 19:01:37 -0400 Subject: [PATCH] feat: replace TTS modal audio embed with custom player controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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\!** 🎵 --- src/tts-modal.js | 204 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 189 insertions(+), 15 deletions(-) diff --git a/src/tts-modal.js b/src/tts-modal.js index b9a34b4..270ed7a 100644 --- a/src/tts-modal.js +++ b/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 }));