diff --git a/src/App.jsx b/src/App.jsx index eb969c2..cd2fdd6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,6 @@ import { lazy, Suspense, useEffect, useRef, useState } from 'react'; import { AppContext, useApp } from './context/AppContext.js'; +import { useAudio } from './hooks/useAudio.js'; import { useBibleReader } from './hooks/useBibleReader.js'; import HomePage from './pages/HomePage.jsx'; @@ -166,126 +167,7 @@ const App = () => { const [importBusy, setImportBusy] = useState(false); const [importError, setImportError] = useState(''); const reader = useBibleReader(currentPage); - const { readerBookAbbrev, readerChapter } = reader; // used by the shared audio player below - const [audioBook, setAudioBook] = useState(bookOptions[0].abbrev); - const [audioNarrator, setAudioNarrator] = useState('souer'); - const [audioState, setAudioState] = useState({ status: 'idle', chapter: 0, total: 0 }); - const [readerAudioState, setReaderAudioState] = useState({ status: 'idle', chapter: 0 }); - const audioRef = useRef(null); - const audioModeRef = useRef('book'); // 'book' | 'reader' — which player owns audioRef - const audioBookRef = useRef(audioBook); - const audioNarratorRef = useRef(audioNarrator); - const audioStateRef = useRef(audioState); - audioBookRef.current = audioBook; - audioNarratorRef.current = audioNarrator; - audioStateRef.current = audioState; - - 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(); - audioRef.current.addEventListener('ended', () => { - if (audioModeRef.current === 'reader') { - setReaderAudioState({ status: 'idle', chapter: 0 }); - return; - } - const nextChapter = audioStateRef.current.chapter + 1; - if (nextChapter > audioStateRef.current.total) { - setAudioState({ status: 'idle', chapter: 0, total: 0 }); - return; - } - playAudioChapter(audioBookRef.current, nextChapter, audioNarratorRef.current).catch(() => - setAudioState({ status: 'error', chapter: 0, total: 0 }), - ); - }); - } - const audioEl = audioRef.current; - audioEl.src = url; - await audioEl.play(); - if (audioModeRef.current === 'reader') { - setReaderAudioState({ status: 'playing', chapter: chapterNum }); - } else { - setAudioState({ status: 'playing', chapter: chapterNum, total }); - } - }; - - const handlePlayBookAudio = async () => { - audioModeRef.current = 'book'; - 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 }); - setReaderAudioState({ status: 'idle', chapter: 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' })); - } - }; - - 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(() => () => { - if (audioRef.current) { - audioRef.current.pause(); - audioRef.current.src = ''; - } - }, []); + const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter); const [studyLayout, setStudyLayout] = useState(() => { const saved = localStorage.getItem('studyLayout'); return saved === 'stacked' ? 'stacked' : 'split'; // 'annotate' (Draw) was removed @@ -2382,13 +2264,8 @@ const deleteProject = (id) => { resumeProject, renameProjectInStorage, deleteProject, - // Audio - audioBook, setAudioBook, - audioNarrator, setAudioNarrator, - audioState, - handlePlayBookAudio, - handleStopBookAudio, - handleToggleBookAudioPause, + // Audio (book player on Home + chapter player in the Reader) + ...audio, // Import importBookAbbrev, setImportBookAbbrev, importTranslation, setImportTranslation, @@ -2401,9 +2278,6 @@ const deleteProject = (id) => { runEpisodeImport, // Reader ...reader, - readerAudioState, - handlePlayReaderAudio, - handleToggleReaderAudioPause, copyVerse, // Setup availableTranslations, diff --git a/src/hooks/useAudio.js b/src/hooks/useAudio.js new file mode 100644 index 0000000..15eb267 --- /dev/null +++ b/src/hooks/useAudio.js @@ -0,0 +1,138 @@ +// Chapter audio playback. The Home page's whole-book player and the Reader's +// chapter player share one