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 { lazy, Suspense, useEffect, useRef, useState } from 'react';
|
||||||
import { AppContext, useApp } from './context/AppContext.js';
|
import { AppContext, useApp } from './context/AppContext.js';
|
||||||
|
import { useAudio } from './hooks/useAudio.js';
|
||||||
import { useBibleReader } from './hooks/useBibleReader.js';
|
import { useBibleReader } from './hooks/useBibleReader.js';
|
||||||
import HomePage from './pages/HomePage.jsx';
|
import HomePage from './pages/HomePage.jsx';
|
||||||
|
|
||||||
@@ -166,126 +167,7 @@ const App = () => {
|
|||||||
const [importBusy, setImportBusy] = useState(false);
|
const [importBusy, setImportBusy] = useState(false);
|
||||||
const [importError, setImportError] = useState('');
|
const [importError, setImportError] = useState('');
|
||||||
const reader = useBibleReader(currentPage);
|
const reader = useBibleReader(currentPage);
|
||||||
const { readerBookAbbrev, readerChapter } = reader; // used by the shared audio player below
|
const audio = useAudio(reader.readerBookAbbrev, reader.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 = '';
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const [studyLayout, setStudyLayout] = useState(() => {
|
const [studyLayout, setStudyLayout] = useState(() => {
|
||||||
const saved = localStorage.getItem('studyLayout');
|
const saved = localStorage.getItem('studyLayout');
|
||||||
return saved === 'stacked' ? 'stacked' : 'split'; // 'annotate' (Draw) was removed
|
return saved === 'stacked' ? 'stacked' : 'split'; // 'annotate' (Draw) was removed
|
||||||
@@ -2382,13 +2264,8 @@ const deleteProject = (id) => {
|
|||||||
resumeProject,
|
resumeProject,
|
||||||
renameProjectInStorage,
|
renameProjectInStorage,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
// Audio
|
// Audio (book player on Home + chapter player in the Reader)
|
||||||
audioBook, setAudioBook,
|
...audio,
|
||||||
audioNarrator, setAudioNarrator,
|
|
||||||
audioState,
|
|
||||||
handlePlayBookAudio,
|
|
||||||
handleStopBookAudio,
|
|
||||||
handleToggleBookAudioPause,
|
|
||||||
// Import
|
// Import
|
||||||
importBookAbbrev, setImportBookAbbrev,
|
importBookAbbrev, setImportBookAbbrev,
|
||||||
importTranslation, setImportTranslation,
|
importTranslation, setImportTranslation,
|
||||||
@@ -2401,9 +2278,6 @@ const deleteProject = (id) => {
|
|||||||
runEpisodeImport,
|
runEpisodeImport,
|
||||||
// Reader
|
// Reader
|
||||||
...reader,
|
...reader,
|
||||||
readerAudioState,
|
|
||||||
handlePlayReaderAudio,
|
|
||||||
handleToggleReaderAudioPause,
|
|
||||||
copyVerse,
|
copyVerse,
|
||||||
// Setup
|
// Setup
|
||||||
availableTranslations,
|
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