diff --git a/frontend/js/app.js b/frontend/js/app.js index 18f5a88..1b75096 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -3,7 +3,8 @@ import { API_BASE_URL, API_BASE_URL_FOR_FILES } from './config.js'; // Shared per-line audio playback state to prevent overlapping playback let currentLineAudio = null; -let currentLineAudioBtn = null; +let currentLinePlayBtn = null; +let currentLineStopBtn = null; // --- Global Inline Notification Helpers --- // const noticeEl = document.getElementById('global-notice'); @@ -396,55 +397,107 @@ async function initializeDialogEditor() { actionsTd.appendChild(generateBtn); // --- NEW: Per-line Play button --- - const playBtn = document.createElement('button'); - playBtn.innerHTML = '⏵'; - playBtn.title = item.audioUrl ? 'Play generated audio' : 'No audio generated yet'; - playBtn.className = 'play-line-btn'; - playBtn.disabled = !item.audioUrl; - playBtn.onclick = () => { + const playPauseBtn = document.createElement('button'); + playPauseBtn.innerHTML = '⏵'; + playPauseBtn.title = item.audioUrl ? 'Play' : 'No audio generated yet'; + playPauseBtn.className = 'play-line-btn'; + playPauseBtn.disabled = !item.audioUrl; + + const stopBtn = document.createElement('button'); + stopBtn.innerHTML = '⏹'; + stopBtn.title = 'Stop'; + stopBtn.className = 'stop-line-btn'; + stopBtn.disabled = !item.audioUrl; + + const setBtnStatesForPlaying = () => { + try { + playPauseBtn.innerHTML = '⏸'; + playPauseBtn.title = 'Pause'; + stopBtn.disabled = false; + } catch (e) { /* detached */ } + }; + const setBtnStatesForPausedOrStopped = () => { + try { + playPauseBtn.innerHTML = '⏵'; + playPauseBtn.title = 'Play'; + } catch (e) { /* detached */ } + }; + + const stopCurrent = () => { + if (currentLineAudio) { + try { currentLineAudio.pause(); currentLineAudio.currentTime = 0; } catch (e) { /* noop */ } + } + if (currentLinePlayBtn) { + try { currentLinePlayBtn.innerHTML = '⏵'; currentLinePlayBtn.title = 'Play'; } catch (e) { /* detached */ } + } + if (currentLineStopBtn) { + try { currentLineStopBtn.disabled = true; } catch (e) { /* detached */ } + } + currentLineAudio = null; + currentLinePlayBtn = null; + currentLineStopBtn = null; + }; + + playPauseBtn.onclick = () => { if (!item.audioUrl) return; const audioUrl = item.audioUrl.startsWith('http') ? item.audioUrl : `${API_BASE_URL_FOR_FILES}${item.audioUrl}`; - // If something is already playing - if (currentLineAudio && !currentLineAudio.paused) { - if (currentLineAudioBtn === playBtn) { - // Same line: ignore click to prevent overlapping - return; - } - // Stop previous audio and re-enable its button - try { - currentLineAudio.pause(); - currentLineAudio.currentTime = 0; - } catch (e) { /* noop */ } - if (currentLineAudioBtn) { - try { currentLineAudioBtn.disabled = false; } catch (e) { /* detached */ } + // If controlling the same line + if (currentLineAudio && currentLinePlayBtn === playPauseBtn) { + if (currentLineAudio.paused) { + // Resume + currentLineAudio.play().then(() => setBtnStatesForPlaying()).catch(err => { + console.error('Audio resume failed:', err); + showNotice('Could not resume audio.', 'error', { timeout: 2000 }); + }); + } else { + // Pause + try { currentLineAudio.pause(); } catch (e) { /* noop */ } + setBtnStatesForPausedOrStopped(); } + return; } + // Switching to a different line: stop previous + if (currentLineAudio) { + stopCurrent(); + } + + // Start new audio const audio = new window.Audio(audioUrl); currentLineAudio = audio; - currentLineAudioBtn = playBtn; - // Disable this play button while playing - playBtn.disabled = true; + currentLinePlayBtn = playPauseBtn; + currentLineStopBtn = stopBtn; const clearState = () => { if (currentLineAudio === audio) { + setBtnStatesForPausedOrStopped(); + try { stopBtn.disabled = true; } catch (e) { /* detached */ } currentLineAudio = null; - currentLineAudioBtn = null; + currentLinePlayBtn = null; + currentLineStopBtn = null; } - try { playBtn.disabled = false; } catch (e) { /* detached */ } }; audio.addEventListener('ended', clearState, { once: true }); audio.addEventListener('error', clearState, { once: true }); - audio.play().catch(err => { + audio.play().then(() => setBtnStatesForPlaying()).catch(err => { console.error('Audio play failed:', err); clearState(); showNotice('Could not play audio.', 'error', { timeout: 2000 }); }); }; - actionsTd.appendChild(playBtn); + + stopBtn.onclick = () => { + // Only acts if this line is the active one + if (currentLineAudio && currentLinePlayBtn === playPauseBtn) { + stopCurrent(); + } + }; + + actionsTd.appendChild(playPauseBtn); + actionsTd.appendChild(stopBtn); // --- NEW: Settings button for speech items --- if (item.type === 'speech') {