frontend: add per-line play/pause/stop controls\n\n- Toggle play/pause on same button, add stop button\n- Maintain shared audio state to prevent overlap and update button states accordingly

This commit is contained in:
Steve White 2025-08-13 00:28:30 -05:00
parent c9593fe6cc
commit 93e0407eac
1 changed files with 80 additions and 27 deletions

View File

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