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:
parent
c9593fe6cc
commit
93e0407eac
|
@ -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
|
||||||
return;
|
currentLineAudio.play().then(() => setBtnStatesForPlaying()).catch(err => {
|
||||||
}
|
console.error('Audio resume failed:', err);
|
||||||
// Stop previous audio and re-enable its button
|
showNotice('Could not resume audio.', 'error', { timeout: 2000 });
|
||||||
try {
|
});
|
||||||
currentLineAudio.pause();
|
} else {
|
||||||
currentLineAudio.currentTime = 0;
|
// Pause
|
||||||
} catch (e) { /* noop */ }
|
try { currentLineAudio.pause(); } catch (e) { /* noop */ }
|
||||||
if (currentLineAudioBtn) {
|
setBtnStatesForPausedOrStopped();
|
||||||
try { currentLineAudioBtn.disabled = false; } catch (e) { /* detached */ }
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Switching to a different line: stop previous
|
||||||
|
if (currentLineAudio) {
|
||||||
|
stopCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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') {
|
||||||
|
|
Loading…
Reference in New Issue