{plan.title}
{formatDayReadings(refs)}
diff --git a/public/sw.js b/public/sw.js index c8798a5..d4a6125 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,9 @@ const SHELL = 'bible-shell-v1'; const API = 'bible-api-v1'; -const BIBLE_ORIGINS = ['https://bible.helloao.org', 'https://bolls.life']; +// Chapter text, interlinear cross-refs, and (BSB) chapter audio all come from +// these origins — cache them so the reader, reading plans, and audio all keep +// working offline once you've visited a passage before. +const BIBLE_ORIGINS = ['https://bible.helloao.org', 'https://bolls.life', 'https://audio.bible.helloao.org']; self.addEventListener('install', (e) => { e.waitUntil( @@ -27,12 +30,14 @@ self.addEventListener('fetch', (e) => { // Never intercept API routes — let Express handle them if (new URL(url).pathname.startsWith('/api/')) return; - // External Bible API: network-first, fall back to cache + // External Bible API (text, cross-refs, audio): network-first, fall back to cache. + // Audio elements often issue Range requests, which come back as 206 Partial + // Content — the Cache API throws on those, so only cache full (200) responses. if (BIBLE_ORIGINS.some(o => url.startsWith(o))) { e.respondWith( fetch(request) .then(res => { - if (res.ok) caches.open(API).then(c => c.put(request, res.clone())); + if (res.status === 200) caches.open(API).then(c => c.put(request, res.clone())); return res; }) .catch(() => diff --git a/src/App.jsx b/src/App.jsx index 43f369a..ef959de 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,7 +8,9 @@ import { useReadingPlans } from './hooks/useReadingPlans.js'; import { useSharedView } from './hooks/useSharedView.js'; import HomePage from './pages/HomePage.jsx'; import { getRemoteReadingProgress, saveRemoteProject } from './syncService.js'; -import { buildExportHtml, loadProjectIndex, switchStorageUser } from './utils/bibleUtils.jsx'; +import { + bookOptions, buildExportHtml, loadProjectIndex, makeId, parseBibleChapter, switchStorageUser, +} from './utils/bibleUtils.jsx'; import { migrateOldStorageKeys, saveProjectToStorage } from './utils/projectStorage.js'; // Less-common pages are split into their own chunks so the first load only @@ -35,6 +37,7 @@ const App = () => { const [saveStatus, setSaveStatus] = useState(''); const saveTimerRef = useRef(null); const [syncStatus, setSyncStatus] = useState(''); // '' | 'syncing' | 'synced' | 'error' + const [studyBuildBusy, setStudyBuildBusy] = useState(false); const reader = useBibleReader(currentPage); const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter); const readingPlans = useReadingPlans(); @@ -123,6 +126,55 @@ const App = () => { setCurrentPage('reader'); }; + // Starts book-mode audio playback at a specific plan day's chapter, then + // shows the Home audio player. Uses handlePlayBookAudioFrom (not the reader + // player) so this works even without opening the reader first. + const listenReadingPlanDay = (bookAbbrev, chapter) => { + audio.handlePlayBookAudioFrom(bookAbbrev, chapter); + goHome(); + }; + + // Builds a new chunk-builder project seeded with a reading-plan day's + // passage — one whole-chapter chunk per chapter in the day — and jumps + // straight into the Study page, skipping the manual chapter/chunk setup. + const studyReadingPlanDay = async (dayRefs, title) => { + setStudyBuildBusy(true); + try { + const chapters = await Promise.all(dayRefs.map(async ({ book, chapter }) => { + const res = await fetch(`https://bible.helloao.org/api/BSB/${book}/${chapter}.json`); + if (!res.ok) throw new Error(`Failed to load ${book} ${chapter}`); + const data = await res.json(); + const verses = parseBibleChapter(data); + const bookName = bookOptions.find((b) => b.abbrev === book)?.name ?? book; + const chunk = { + id: makeId(), + startVerse: verses[0]?.number ?? 1, + endVerse: verses.at(-1)?.number ?? 1, + spilloverEndVerse: null, + observation: '', interpretation: '', application: '', + crossReferences: [], greekWords: [], generalNotes: '', + episodeNumber: '', episodeTitle: '', finalScript: '', tags: [], + }; + return { book: bookName, bookAbbrev: book, chapter: String(chapter), verses, chunks: [chunk] }; + })); + const newProject = { + id: makeId(), + title: title || 'Reading Plan Study', + translation: 'BSB', + lastEdited: Date.now(), + selectedChunkId: chapters[0]?.chunks[0]?.id ?? null, + chapters, + }; + setProject(newProject); + setCurrentPage('study'); + } catch (err) { + console.error('studyReadingPlanDay error:', err); + window.alert("Couldn't load today's passage. Please try again."); + } finally { + setStudyBuildBusy(false); + } + }; + const openNewProject = () => { setProject(null); setCurrentPage('setup'); @@ -172,6 +224,9 @@ const App = () => { openBibleReader, openReadingPlans, readReadingPlanDay, + listenReadingPlanDay, + studyReadingPlanDay, + studyBuildBusy, openImportProject, openNewProject, // Audio (book player on Home + chapter player in the Reader) diff --git a/src/hooks/useAudio.js b/src/hooks/useAudio.js index 018e885..acbf08b 100644 --- a/src/hooks/useAudio.js +++ b/src/hooks/useAudio.js @@ -67,6 +67,20 @@ export function useAudio(readerBookAbbrev, readerChapter) { } }; + // Starts book-mode playback at an arbitrary chapter (e.g. a reading plan's + // "today" chapter) instead of always chapter 1. Explicit args avoid relying + // on reader state that may not have re-rendered yet if called right after + // navigating there in the same event handler. + const handlePlayBookAudioFrom = async (abbrev, startChapter) => { + audioModeRef.current = 'book'; + setAudioBook(abbrev); + try { + await playAudioChapter(abbrev, startChapter, audioNarrator); + } catch { + setAudioState({ status: 'error', chapter: 0, total: 0 }); + } + }; + const handleStopBookAudio = () => { if (audioRef.current) { audioRef.current.pause(); @@ -161,6 +175,7 @@ export function useAudio(readerBookAbbrev, readerChapter) { audioState, audioProgress, handlePlayBookAudio, + handlePlayBookAudioFrom, handleStopBookAudio, handleToggleBookAudioPause, handleSkipBookChapter, diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index 4625a9e..98a971d 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -21,6 +21,9 @@ export default function HomePage() { toggleDay, openReadingPlans, readReadingPlanDay, + listenReadingPlanDay, + studyReadingPlanDay, + studyBuildBusy, audioBook, setAudioBook, audioNarrator, setAudioNarrator, audioState, @@ -161,9 +164,61 @@ export default function HomePage() { readingActivity={readingActivity} openReadingPlans={openReadingPlans} readReadingPlanDay={readReadingPlanDay} + listenReadingPlanDay={listenReadingPlanDay} + studyReadingPlanDay={studyReadingPlanDay} + studyBuildBusy={studyBuildBusy} toggleDay={toggleDay} /> + {/* BSB audio player — shown regardless of whether there are any study projects yet, + so "Listen" from a reading plan always has somewhere visible to land. */} +
No projects yet
@@ -252,52 +307,6 @@ export default function HomePage() {{plan.title}
{formatDayReadings(refs)}