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>
98 lines
3.2 KiB
JavaScript
98 lines
3.2 KiB
JavaScript
const SHELL = 'bible-shell-v2';
|
|
const API = 'bible-api-v1';
|
|
// 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(
|
|
caches.open(SHELL)
|
|
.then(c => c.addAll(['/', '/index.html']))
|
|
.then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
e.waitUntil(
|
|
caches.keys()
|
|
.then(keys => Promise.all(
|
|
keys.filter(k => k !== SHELL && k !== API).map(k => caches.delete(k))
|
|
))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (e) => {
|
|
const { request } = e;
|
|
const url = request.url;
|
|
|
|
// Never intercept this app's own API routes — let Express handle them. (Only same-origin ones: the
|
|
// Bible text at bible.helloao.org/api/... must pass through to the offline cache below.)
|
|
const { origin, pathname } = new URL(url);
|
|
if (origin === self.location.origin && pathname.startsWith('/api/')) return;
|
|
|
|
// 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 => {
|
|
// Clone *now*: once the page starts reading the body a later clone() throws and nothing is saved.
|
|
if (res.status === 200) {
|
|
const copy = res.clone();
|
|
e.waitUntil(caches.open(API).then(c => c.put(request, copy)).catch(() => {}));
|
|
}
|
|
return res;
|
|
})
|
|
.catch(() =>
|
|
caches.open(API).then(c => c.match(request)).then(
|
|
hit => hit ?? new Response('{"error":"offline"}', {
|
|
status: 503, headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
)
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// App-shell navigation: network-first, fall back to cached index.html
|
|
if (request.mode === 'navigate') {
|
|
e.respondWith(
|
|
fetch(request)
|
|
.then(res => {
|
|
if (res.ok) {
|
|
const copy = res.clone();
|
|
e.waitUntil(caches.open(SHELL).then(c => c.put(request, copy)).catch(() => {}));
|
|
}
|
|
return res;
|
|
})
|
|
.catch(() =>
|
|
caches.open(SHELL).then(c =>
|
|
c.match('/index.html').then(hit => hit ?? c.match('/'))
|
|
)
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Same-origin static assets (hashed JS/CSS/images): cache-first
|
|
if (url.startsWith(self.location.origin)) {
|
|
e.respondWith(
|
|
caches.open(SHELL).then(c =>
|
|
c.match(request).then(hit => {
|
|
if (hit) return hit;
|
|
return fetch(request).then(res => {
|
|
if (res.ok) {
|
|
const copy = res.clone();
|
|
e.waitUntil(c.put(request, copy).catch(() => {}));
|
|
}
|
|
return res;
|
|
});
|
|
})
|
|
)
|
|
);
|
|
}
|
|
});
|