Files
study-bible-project/src/utils/offline.js
T
nmemmertandClaude Sonnet 5.5 67645cf1df App quality: offline badge, per-note history, keyboard study mode, notes import
- Study cards say whether the Bible text works offline, with a button to save
  the missing book (notes are always kept on the device).
- Each note keeps its last 12 versions with Restore (History under each note).
- Keyboard-only study: 1/2/3 write in Observe/Interpret/Apply, C/W/X/P switch
  resource tabs, V compares, Esc leaves a note box; listed under "?".
- Export > Import notes: bring Word/Markdown/text notes into the matching
  chunks without overwriting anything; can mark them as not your own.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:01:11 -04:00

61 lines
2.9 KiB
JavaScript

// "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 };
}