Extract shared audio player into hooks/useAudio
Build and Push Docker Image / build-and-push (push) Failing after 2m25s
Build and Push Docker Image / build-and-push (push) Failing after 2m25s
The Home page's whole-book player and the Reader's chapter player share one <audio> element, so they move together. App now just calls useAudio(readerBook, readerChapter) and spreads the result into context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ade7f1af6f
commit
009fa1e78f
2 files changed
+142
-130
No files matched your search
+4
-130
@@ -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,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// Chapter audio playback. The Home page's whole-book player and the Reader's
|
||||
// chapter player share one <audio> element; audioModeRef tracks which owns it.
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { bookOptions } from '../utils/bibleUtils.jsx';
|
||||
|
||||
export function useAudio(readerBookAbbrev, readerChapter) {
|
||||
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 = '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audioBook, setAudioBook,
|
||||
audioNarrator, setAudioNarrator,
|
||||
audioState,
|
||||
handlePlayBookAudio,
|
||||
handleStopBookAudio,
|
||||
handleToggleBookAudioPause,
|
||||
readerAudioState,
|
||||
handlePlayReaderAudio,
|
||||
handleToggleReaderAudioPause,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user