Add Outline stage, parallel reading, offline saves, backup, sync-conflict handling and more
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>
This commit is contained in:
1 parent
1edbd364ea
commit
d13f55faf7
65 files changed
+2947
-208
No files matched your search
@@ -0,0 +1,52 @@
|
||||
// The Bibles the Reader can show, and one way to fetch a chapter of any of them. Used by the
|
||||
// main reading text, the side-by-side view, and the verse "Compare" tab.
|
||||
import { CHAPTER_COUNTS, parseBibleChapter } from './bibleUtils.jsx';
|
||||
|
||||
export const READER_TRANSLATIONS = [
|
||||
{ id: 'BSB', name: 'Berean Standard Bible' },
|
||||
{ id: 'ESV', name: 'English Standard Version', licensed: true,
|
||||
notice: 'Scripture quotations are from the ESV® Bible (The Holy Bible, English Standard Version®), © 2001 by Crossway, a publishing ministry of Good News Publishers. Used by permission. All rights reserved.' },
|
||||
{ id: 'NET', name: 'NET Bible', helloao: 'eng_net',
|
||||
notice: 'Scripture quoted by permission. Quotations designated (NET) are from the NET Bible® copyright ©1996-2017 by Biblical Studies Press, L.L.C. http://netbible.com All rights reserved.' },
|
||||
{ id: 'NLT', name: 'New Living Translation', licensed: true,
|
||||
notice: 'Holy Bible, New Living Translation, Copyright © 2014, Tyndale House Publishers. All rights reserved. tyndale.com' },
|
||||
{ id: 'CSB', name: 'Christian Standard Bible', licensed: true,
|
||||
notice: 'Christian Standard Bible® and CSB® are federally registered trademarks of Holman Bible Publishers. All rights reserved. bhpublishinggroup.com' },
|
||||
{ id: 'NASB', name: 'New American Standard Bible', licensed: true,
|
||||
notice: 'New American Standard Bible®, Copyright © 1995, The Lockman Foundation. All rights reserved. lockman.org' },
|
||||
];
|
||||
|
||||
export const translationName = (id) => READER_TRANSLATIONS.find((t) => t.id === id)?.name ?? id;
|
||||
export const isLicensed = (id) => READER_TRANSLATIONS.find((t) => t.id === id)?.licensed === true;
|
||||
|
||||
// Free translations are kept for the visit so flipping between chapters, the side-by-side
|
||||
// view and Compare don't re-download. Licensed text is never kept here (their terms limit
|
||||
// storage); the server holds it briefly.
|
||||
const freeChapters = new Map(); // "BSB/JHN/3" -> { verses, totalChapters }
|
||||
|
||||
export const clearChapterCache = () => freeChapters.clear();
|
||||
|
||||
// -> { verses: [{ number, text }], copyright, totalChapters }. Throws Error(message) on failure.
|
||||
// Pass `signal` to cancel a request that is no longer wanted.
|
||||
export async function fetchChapter(translation, bookAbbrev, chapter, { signal } = {}) {
|
||||
const source = READER_TRANSLATIONS.find((t) => t.id === translation) ?? READER_TRANSLATIONS[0];
|
||||
let totalChapters = CHAPTER_COUNTS[bookAbbrev] ?? chapter;
|
||||
if (!source.licensed) {
|
||||
const key = `${source.id}/${bookAbbrev}/${chapter}`;
|
||||
const hit = freeChapters.get(key);
|
||||
if (hit) return { verses: hit.verses, copyright: '', totalChapters: hit.totalChapters };
|
||||
const res = await fetch(`https://bible.helloao.org/api/${source.helloao ?? source.id}/${bookAbbrev}/${chapter}.json`, { signal });
|
||||
if (!res.ok) throw new Error('Unable to load chapter.');
|
||||
const data = await res.json();
|
||||
const verses = parseBibleChapter(data);
|
||||
if (!Array.isArray(verses) || verses.length === 0) throw new Error('Invalid Bible data returned.');
|
||||
totalChapters = data.book?.numberOfChapters ?? totalChapters;
|
||||
freeChapters.set(key, { verses, totalChapters });
|
||||
return { verses, copyright: '', totalChapters };
|
||||
}
|
||||
const res = await fetch(`/api/bible/${source.id}/${bookAbbrev}/${chapter}`, { credentials: 'include', signal });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || `Unable to load ${source.id}.`);
|
||||
if (!Array.isArray(data.verses) || data.verses.length === 0) throw new Error('Invalid Bible data returned.');
|
||||
return { verses: data.verses, copyright: data.copyright ?? '', totalChapters };
|
||||
}
|
||||
Reference in new issue
Block a user