// "Save for offline": fetch chapters ahead of time so they open without a connection. Fetching is // all it takes — the service worker (public/sw.js) keeps every Bible-text response it sees. Only the // free translations (BSB, NET) can be saved; the licensed ones' terms don't allow bulk copies. import { CHAPTER_COUNTS } from './bibleUtils.jsx'; import { fetchChapter, isLicensed } from './bibleText.js'; const KEY = 'offline-saved'; // { 'BSB': ['JHN', 'PSA'] }: whole books saved on this device, per translation. export function loadSavedBooks() { try { const raw = JSON.parse(localStorage.getItem(KEY) || '{}'); return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}; } catch { return {}; } } export function markBookSaved(translation, book) { const all = loadSavedBooks(); all[translation] = [...new Set([...(all[translation] ?? []), book])]; try { localStorage.setItem(KEY, JSON.stringify(all)); } catch { /* storage full or unavailable */ } return all; } export const canSaveOffline = (translation) => !isLicensed(translation); export const bookRefs = (book) => Array.from({ length: CHAPTER_COUNTS[book] ?? 0 }, (_, i) => ({ book, chapter: i + 1 })); export const bibleRefs = () => Object.keys(CHAPTER_COUNTS).flatMap(bookRefs); // Fetches every chapter in `refs` ([{ book, chapter }]) a few at a time, calling onProgress(done, total) // as each lands. Resolves { done, failed } — a chapter that fails doesn't stop the rest. Pass `signal` // to stop early. export async function downloadChapters(translation, refs, { onProgress, signal, concurrency = 4 } = {}) { const queue = [...refs]; let done = 0; let failed = 0; const worker = async () => { while (queue.length > 0 && !signal?.aborted) { const { book, chapter } = queue.shift(); try { await fetchChapter(translation, book, chapter, { signal }); done += 1; } catch { if (!signal?.aborted) failed += 1; } onProgress?.(done + failed, refs.length); } }; await Promise.all(Array.from({ length: Math.min(concurrency, refs.length) }, worker)); return { done, failed }; } // What a study needs from the network to open offline. Your notes are always kept on this device; only the // Bible text must have been saved. `saved` is loadSavedBooks(). -> { state: 'ready' | 'partial' | 'licensed', // translation, missing: [{ abbrev, name }] } export function offlineStatus(project, saved = loadSavedBooks()) { const translation = project?.translation ?? 'BSB'; if (!canSaveOffline(translation)) return { state: 'licensed', translation, missing: [] }; const books = []; for (const chapter of project?.chapters ?? []) { if (chapter.bookAbbrev && !books.some((b) => b.abbrev === chapter.bookAbbrev)) books.push({ abbrev: chapter.bookAbbrev, name: chapter.book }); } const have = saved[translation] ?? []; const missing = books.filter((b) => !have.includes(b.abbrev)); return { state: missing.length ? 'partial' : 'ready', translation, missing }; }