From 0f08721b484f8bd4910808c2a356a5f9d8be7695 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 11 Jun 2026 16:15:28 -0400 Subject: [PATCH] Add BSB full-book audio player card to home page Co-Authored-By: Claude Sonnet 4.6 --- src/App.jsx | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/src/App.jsx b/src/App.jsx index 55ac323..95b8c61 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -892,6 +892,77 @@ const App = () => { const [homeTagFilter, setHomeTagFilter] = useState(''); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); + const [audioBook, setAudioBook] = useState(bookOptions[0].abbrev); + const [audioNarrator, setAudioNarrator] = useState('souer'); + const [audioState, setAudioState] = useState({ status: 'idle', chapter: 0, total: 0 }); + const audioRef = useRef(null); + + const playAudioChapter = async (abbrev, chapterNum, narrator) => { + const res = await fetch(`https://bible.helloao.org/api/BSB/${abbrev}/${chapterNum}.json`); + if (!res.ok) throw new Error('Failed to load chapter audio'); + const data = await res.json(); + const total = data.book?.numberOfChapters ?? chapterNum; + const url = data.thisChapterAudioLinks?.[narrator]; + if (!url) throw new Error('Audio not available for this narrator'); + if (!audioRef.current) audioRef.current = new Audio(); + const audioEl = audioRef.current; + audioEl.src = url; + await audioEl.play(); + setAudioState({ status: 'playing', chapter: chapterNum, total }); + }; + + const handlePlayBookAudio = async () => { + try { + await playAudioChapter(audioBook, 1, audioNarrator); + } catch { + setAudioState({ status: 'error', chapter: 0, total: 0 }); + } + }; + + const handleStopBookAudio = () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + } + setAudioState({ status: 'idle', chapter: 0, total: 0 }); + }; + + const handleToggleBookAudioPause = () => { + if (!audioRef.current) return; + if (audioRef.current.paused) { + audioRef.current.play(); + setAudioState((prev) => ({ ...prev, status: 'playing' })); + } else { + audioRef.current.pause(); + setAudioState((prev) => ({ ...prev, status: 'paused' })); + } + }; + + useEffect(() => { + const audioEl = audioRef.current; + if (!audioEl) return undefined; + const handleEnded = () => { + setAudioState((prev) => { + const nextChapter = prev.chapter + 1; + if (nextChapter > prev.total) { + return { status: 'idle', chapter: 0, total: 0 }; + } + playAudioChapter(audioBook, nextChapter, audioNarrator).catch(() => + setAudioState({ status: 'error', chapter: 0, total: 0 }), + ); + return prev; + }); + }; + audioEl.addEventListener('ended', handleEnded); + return () => audioEl.removeEventListener('ended', handleEnded); + }, [audioBook, audioNarrator]); + + useEffect(() => () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + } + }, []); const [studyLayout, setStudyLayout] = useState( () => localStorage.getItem('studyLayout') || 'stacked', ); // 'stacked' | 'split' @@ -2723,6 +2794,67 @@ const restoreRemoteProject = async (id) => { ); })()} +
+
+

Listen to BSB Audio

+

+ {audioState.status === 'idle' || audioState.status === 'error' + ? 'Play a full book of the Berean Standard Bible.' + : `Playing ${bookOptions.find((b) => b.abbrev === audioBook)?.name} — chapter ${audioState.chapter} of ${audioState.total}`} +

+ {audioState.status === 'error' && ( +

Couldn't load audio for this book/narrator.

+ )} +
+
+ + + {audioState.status === 'playing' || audioState.status === 'paused' ? ( + <> + + + + ) : ( + + )} +
+
{projectIndex .slice()