Study - New Outline stage (hierarchical I./A./1. points, linked to chunks, in all exports); the chunk stage is now "Chunks" - Home cards: passages shown as ranges, tags capped with "+N more" Reader - Side-by-side translations and a Compare tab per verse - Typeface, line spacing, verse-number settings; text and column scale with the screen - Highlights/bookmarks sheet with named colours (synced); BSB footnote spacing fix - Cmd-K palette (passages, studies, notes, marks), keyboard shortcuts and "?" help Flow and safety - Toasts with Undo replace most confirm()/alert() - Back/Forward move between pages - Server refuses saves over a newer copy (409); keep-mine / use-other banner; opening a study no longer re-stamps it - Latest chapter request wins when flipping quickly Offline and data - Save books, plan days or a whole BSB/NET for offline - Service worker fixes: helloao /api/ URLs were skipped and responses cloned too late - Settings: download/restore a full backup Accessibility - Sheets trap focus and close on Escape, skip link, focus ring, reduced motion, theme contrast raised to 4.5:1 (with a test) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
46 lines
2.1 KiB
JavaScript
46 lines
2.1 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 };
|
|
}
|