Connect reading plans to study/audio, cache offline audio, add journal export
Build and Push Docker Image / build-and-push (push) Successful in 4m33s
Build and Push Docker Image / build-and-push (push) Successful in 4m33s
Reading plan days now offer "Study this" (seeds a new chunk-builder project with the day's full passage and jumps straight into the Study editor) and "Listen" (starts book-mode audio at the right chapter via a new handlePlayBookAudioFrom, avoiding a stale-closure bug the reader-state approach would hit). Fixed the "Listen to BSB Audio" player only rendering when a study project existed, leaving Listen with nowhere to show up. The service worker now caches audio.bible.helloao.org (the actual host serving narrator audio, not just the text/cross-ref API) and no longer tries to cache 206 Partial Content responses, which the Cache API rejects and was throwing unhandled rejections for every range-requested audio file. Added a reading journal export (Settings > My reading journal): gathers bookmarks, text highlights, and completed plans, fetches their verse text, and offers a .docx download or a print-to-PDF page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1dd6aef6a5
commit
787edcbe73
8 files changed
+604
-105
No files matched your search
+109
-30
@@ -6,6 +6,7 @@ import {
|
||||
HeadingLevel,
|
||||
Packer,
|
||||
Paragraph,
|
||||
ShadingType,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
@@ -26,6 +27,35 @@ const DOCX_ACCENT = 'C9A84C'; // antique gold
|
||||
const DOCX_HEADING_FONT = 'Cormorant Garamond';
|
||||
const DOCX_BODY_FONT = 'Lora';
|
||||
|
||||
const DOCX_STYLES = {
|
||||
default: {
|
||||
document: { run: { font: DOCX_BODY_FONT, color: '0A0A08' } },
|
||||
},
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: 'Title',
|
||||
name: 'Title',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: { font: DOCX_HEADING_FONT, size: 56, bold: true, color: '0A0A08' },
|
||||
},
|
||||
{
|
||||
id: 'Heading1',
|
||||
name: 'Heading 1',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: { font: DOCX_HEADING_FONT, size: 36, bold: true, color: '0A0A08' },
|
||||
},
|
||||
{
|
||||
id: 'Heading2',
|
||||
name: 'Heading 2',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: { font: DOCX_HEADING_FONT, size: 30, bold: true, color: '0A0A08' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function sectionLabel(text) {
|
||||
return new Paragraph({
|
||||
spacing: { before: 240, after: 80 },
|
||||
@@ -182,36 +212,85 @@ export async function buildStudyDocxBlob(project, chapterSummary) {
|
||||
});
|
||||
});
|
||||
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: {
|
||||
document: { run: { font: DOCX_BODY_FONT, color: '0A0A08' } },
|
||||
},
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: 'Title',
|
||||
name: 'Title',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: { font: DOCX_HEADING_FONT, size: 56, bold: true, color: '0A0A08' },
|
||||
},
|
||||
{
|
||||
id: 'Heading1',
|
||||
name: 'Heading 1',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: { font: DOCX_HEADING_FONT, size: 36, bold: true, color: '0A0A08' },
|
||||
},
|
||||
{
|
||||
id: 'Heading2',
|
||||
name: 'Heading 2',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: { font: DOCX_HEADING_FONT, size: 30, bold: true, color: '0A0A08' },
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [{ children }],
|
||||
const doc = new Document({ styles: DOCX_STYLES, sections: [{ children }] });
|
||||
return Packer.toBlob(doc);
|
||||
}
|
||||
|
||||
// A journal of bookmarks, text highlights, and completed reading plans — see
|
||||
// utils/journalExport.js for how this data is gathered.
|
||||
export async function buildJournalDocxBlob({ bookmarks, highlights, completedPlans }) {
|
||||
const children = [
|
||||
new Paragraph({ text: 'My Reading Journal', heading: HeadingLevel.TITLE }),
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: `Generated ${new Date().toLocaleDateString()}`, italics: true, color: '7A7060' })],
|
||||
spacing: { after: 300 },
|
||||
}),
|
||||
];
|
||||
|
||||
const heading = (text) => new Paragraph({
|
||||
text,
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: DOCX_ACCENT, space: 4 } },
|
||||
spacing: { before: 360, after: 160 },
|
||||
});
|
||||
|
||||
if (completedPlans.length > 0) {
|
||||
children.push(heading('Reading Plans Completed'));
|
||||
completedPlans.forEach(({ plan, completedAt }) => {
|
||||
children.push(new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: `${plan.icon ?? '✓'} ${plan.title}`, bold: true, color: DOCX_ACCENT }),
|
||||
new TextRun({ text: ` Completed ${new Date(completedAt).toLocaleDateString()} · ${plan.days} days`, italics: true, color: '7A7060' }),
|
||||
],
|
||||
spacing: { after: 80 },
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (bookmarks.length > 0) {
|
||||
children.push(heading('Bookmarked Verses'));
|
||||
bookmarks.forEach((b) => {
|
||||
children.push(new Paragraph({
|
||||
children: [new TextRun({ text: `${b.bookName} ${b.chapter}:${b.verse}`, bold: true, color: DOCX_ACCENT })],
|
||||
spacing: { before: 160 },
|
||||
}));
|
||||
if (b.text) {
|
||||
children.push(new Paragraph({ children: [new TextRun({ text: b.text, italics: true })], spacing: { after: 40 } }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (highlights.length > 0) {
|
||||
children.push(heading('Highlighted Passages'));
|
||||
highlights.forEach((h) => {
|
||||
children.push(new Paragraph({
|
||||
children: [new TextRun({ text: `${h.bookName} ${h.chapter}:${h.verse}`, bold: true, color: DOCX_ACCENT })],
|
||||
spacing: { before: 160 },
|
||||
}));
|
||||
if (h.fullText) {
|
||||
children.push(new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: h.fullText.slice(0, h.startOffset), italics: true }),
|
||||
new TextRun({
|
||||
text: h.excerpt ?? '',
|
||||
italics: true,
|
||||
bold: true,
|
||||
shading: { type: ShadingType.CLEAR, fill: (h.color || '#FFF176').replace('#', '') },
|
||||
}),
|
||||
new TextRun({ text: h.fullText.slice(h.endOffset), italics: true }),
|
||||
],
|
||||
spacing: { after: 40 },
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (completedPlans.length + bookmarks.length + highlights.length === 0) {
|
||||
children.push(new Paragraph({
|
||||
text: "Nothing saved yet — bookmark a verse, highlight some text, or finish a reading plan to build your journal.",
|
||||
}));
|
||||
}
|
||||
|
||||
const doc = new Document({ styles: DOCX_STYLES, sections: [{ children }] });
|
||||
return Packer.toBlob(doc);
|
||||
}
|
||||
Reference in new issue
Block a user