Add chapter audio playback to Bible reader, auto-matching selected book/chapter

This commit is contained in:
nmemmert
2026-06-15 08:32:15 -04:00
parent 1733df7879
commit 405cf7218b
+97
View File
@@ -904,7 +904,9 @@ const App = () => {
const [audioBook, setAudioBook] = useState(bookOptions[0].abbrev); const [audioBook, setAudioBook] = useState(bookOptions[0].abbrev);
const [audioNarrator, setAudioNarrator] = useState('souer'); const [audioNarrator, setAudioNarrator] = useState('souer');
const [audioState, setAudioState] = useState({ status: 'idle', chapter: 0, total: 0 }); const [audioState, setAudioState] = useState({ status: 'idle', chapter: 0, total: 0 });
const [readerAudioState, setReaderAudioState] = useState({ status: 'idle', chapter: 0 });
const audioRef = useRef(null); const audioRef = useRef(null);
const audioModeRef = useRef('book'); // 'book' | 'reader' — which player owns audioRef
const audioBookRef = useRef(audioBook); const audioBookRef = useRef(audioBook);
const audioNarratorRef = useRef(audioNarrator); const audioNarratorRef = useRef(audioNarrator);
const audioStateRef = useRef(audioState); const audioStateRef = useRef(audioState);
@@ -922,6 +924,10 @@ const App = () => {
if (!audioRef.current) { if (!audioRef.current) {
audioRef.current = new Audio(); audioRef.current = new Audio();
audioRef.current.addEventListener('ended', () => { audioRef.current.addEventListener('ended', () => {
if (audioModeRef.current === 'reader') {
setReaderAudioState({ status: 'idle', chapter: 0 });
return;
}
const nextChapter = audioStateRef.current.chapter + 1; const nextChapter = audioStateRef.current.chapter + 1;
if (nextChapter > audioStateRef.current.total) { if (nextChapter > audioStateRef.current.total) {
setAudioState({ status: 'idle', chapter: 0, total: 0 }); setAudioState({ status: 'idle', chapter: 0, total: 0 });
@@ -935,10 +941,15 @@ const App = () => {
const audioEl = audioRef.current; const audioEl = audioRef.current;
audioEl.src = url; audioEl.src = url;
await audioEl.play(); await audioEl.play();
if (audioModeRef.current === 'reader') {
setReaderAudioState({ status: 'playing', chapter: chapterNum });
} else {
setAudioState({ status: 'playing', chapter: chapterNum, total }); setAudioState({ status: 'playing', chapter: chapterNum, total });
}
}; };
const handlePlayBookAudio = async () => { const handlePlayBookAudio = async () => {
audioModeRef.current = 'book';
try { try {
await playAudioChapter(audioBook, 1, audioNarrator); await playAudioChapter(audioBook, 1, audioNarrator);
} catch { } catch {
@@ -952,6 +963,7 @@ const App = () => {
audioRef.current.src = ''; audioRef.current.src = '';
} }
setAudioState({ status: 'idle', chapter: 0, total: 0 }); setAudioState({ status: 'idle', chapter: 0, total: 0 });
setReaderAudioState({ status: 'idle', chapter: 0 });
}; };
const handleToggleBookAudioPause = () => { const handleToggleBookAudioPause = () => {
@@ -965,6 +977,43 @@ const App = () => {
} }
}; };
const handlePlayReaderAudio = async () => {
audioModeRef.current = 'reader';
try {
await playAudioChapter(readerBookAbbrev, readerChapter, audioNarrator);
} catch {
setReaderAudioState({ status: 'error', chapter: 0 });
}
};
const handleToggleReaderAudioPause = () => {
if (!audioRef.current) return;
if (audioRef.current.paused) {
audioRef.current.play();
setReaderAudioState((prev) => ({ ...prev, status: 'playing' }));
} else {
audioRef.current.pause();
setReaderAudioState((prev) => ({ ...prev, status: 'paused' }));
}
};
// Auto-switch the reader's audio to match the book/chapter currently being read.
useEffect(() => {
if (audioModeRef.current !== 'reader') return;
if (readerAudioState.status !== 'playing' && readerAudioState.status !== 'paused') return;
if (readerAudioState.chapter === readerChapter) return;
const wasPaused = readerAudioState.status === 'paused';
playAudioChapter(readerBookAbbrev, readerChapter, audioNarrator)
.then(() => {
if (wasPaused && audioRef.current) {
audioRef.current.pause();
setReaderAudioState((prev) => ({ ...prev, status: 'paused' }));
}
})
.catch(() => setReaderAudioState({ status: 'error', chapter: 0 }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [readerBookAbbrev, readerChapter]);
useEffect(() => () => { useEffect(() => () => {
if (audioRef.current) { if (audioRef.current) {
audioRef.current.pause(); audioRef.current.pause();
@@ -3099,6 +3148,54 @@ const restoreRemoteProject = async (id) => {
</div> </div>
</div> </div>
<div className="mb-6 flex flex-wrap items-center gap-3 rounded-3xl border border-slate-200 bg-white p-4 shadow-panel">
<div className="flex-1">
<h3 className="text-sm font-semibold text-slate-900">Listen to this chapter</h3>
<p className="mt-1 text-xs text-slate-500">
{readerAudioState.status === 'error'
? "Couldn't load audio for this chapter/narrator."
: `Audio follows ${readerBook?.name} ${readerChapter} as you browse.`}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<select
value={audioNarrator}
onChange={(e) => setAudioNarrator(e.target.value)}
className="rounded-xl border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
>
<option value="david">David</option>
<option value="hays">Hays</option>
<option value="souer">Souer</option>
</select>
{readerAudioState.status === 'playing' || readerAudioState.status === 'paused' ? (
<>
<button
type="button"
onClick={handleToggleReaderAudioPause}
className="rounded-xl bg-sky-600 px-3 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-sky-500"
>
{readerAudioState.status === 'paused' ? 'Resume' : 'Pause'}
</button>
<button
type="button"
onClick={handleStopBookAudio}
className="rounded-xl border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50"
>
Stop
</button>
</>
) : (
<button
type="button"
onClick={handlePlayReaderAudio}
className="rounded-xl bg-sky-600 px-3 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-sky-500"
>
Play
</button>
)}
</div>
</div>
<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-panel"> <div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-panel">
<h2 className="mb-4 text-xl font-semibold text-slate-900"> <h2 className="mb-4 text-xl font-semibold text-slate-900">
{readerBook?.name} {readerChapter} <span className="text-sm font-normal text-slate-500">(BSB)</span> {readerBook?.name} {readerChapter} <span className="text-sm font-normal text-slate-500">(BSB)</span>