- 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>
61 lines
2.9 KiB
JavaScript
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 };
|
|
}
|