Add chapter audio playback to Bible reader, auto-matching selected book/chapter
This commit is contained in:
+98
-1
@@ -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();
|
||||||
setAudioState({ status: 'playing', chapter: chapterNum, total });
|
if (audioModeRef.current === 'reader') {
|
||||||
|
setReaderAudioState({ status: 'playing', chapter: chapterNum });
|
||||||
|
} else {
|
||||||
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user