- Commentary and study aids wait for 15 words of your own Observe notes. - Insights page: this week's own work, themes across studies, words you keep studying, what slipped in recalls, open follow-ups. - Recall can record what slipped; session notes (teach it back, who came, group notes, follow-ups); "Pray about this" from Apply. - Compare any two passages side by side. - Handout layouts: standard, questions only, large print, note-taking. - Search understands "in <book>", "last week/month/year" and a year. - Home shows the last backup age and nudges after two weeks. - Phone: the stage bar scrolls instead of clipping Script and Export. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
103 lines
5.8 KiB
JavaScript
103 lines
5.8 KiB
JavaScript
// "Download my data" / "Restore": everything you've made in one JSON file you keep — studies, bookmarks,
|
||
// highlights (and what you named the colours), ink notes and reading plans. Restoring only ever adds or
|
||
// updates; it never deletes anything you already have here.
|
||
import { loadProjectIndex } from './bibleUtils.jsx';
|
||
import { loadProjectById, saveProjectToStorage } from './projectStorage.js';
|
||
|
||
export const BACKUP_APP = 'bible-study-project';
|
||
export const BACKUP_VERSION = 1;
|
||
|
||
// `reader` is what the app holds in memory: { bookmarks, highlights, colorLabels, ink }.
|
||
export function buildBackup({ reader, readingPlans, prayers = [], email = null, now = new Date() }) {
|
||
const projects = loadProjectIndex().map((e) => loadProjectById(e.id)).filter(Boolean);
|
||
return {
|
||
app: BACKUP_APP,
|
||
version: BACKUP_VERSION,
|
||
exportedAt: now.toISOString(),
|
||
account: email,
|
||
projects,
|
||
reader: {
|
||
bookmarks: reader?.bookmarks ?? {},
|
||
bookmarkNotes: reader?.bookmarkNotes ?? {},
|
||
highlights: reader?.highlights ?? [],
|
||
colorLabels: reader?.colorLabels ?? {},
|
||
ink: reader?.ink ?? {},
|
||
},
|
||
readingPlans: readingPlans ?? { progress: [], customPlans: [], activity: [] },
|
||
prayers: (prayers ?? []).filter((p) => p && !p.deleted),
|
||
};
|
||
}
|
||
|
||
export const backupFileName = (now = new Date()) => `bible-study-backup-${now.toISOString().slice(0, 10)}.json`;
|
||
|
||
// -> { ok: true, backup, summary } or { ok: false, error }. Checks the file really is one of ours.
|
||
export function validateBackup(data) {
|
||
if (!data || typeof data !== 'object' || data.app !== BACKUP_APP) return { ok: false, error: 'That doesn’t look like a Bible Study backup file.' };
|
||
if (typeof data.version !== 'number' || data.version > BACKUP_VERSION) return { ok: false, error: 'This backup was made by a newer version of the app.' };
|
||
const projects = Array.isArray(data.projects) ? data.projects.filter((p) => p && typeof p.id === 'string' && p.title && Array.isArray(p.chapters)) : [];
|
||
const reader = data.reader && typeof data.reader === 'object' ? data.reader : {};
|
||
const bookmarks = reader.bookmarks && typeof reader.bookmarks === 'object' && !Array.isArray(reader.bookmarks) ? reader.bookmarks : {};
|
||
const bookmarkNotes = reader.bookmarkNotes && typeof reader.bookmarkNotes === 'object' && !Array.isArray(reader.bookmarkNotes) ? reader.bookmarkNotes : {};
|
||
const highlights = Array.isArray(reader.highlights) ? reader.highlights.filter((h) => h && typeof h.id === 'string') : [];
|
||
const colorLabels = reader.colorLabels && typeof reader.colorLabels === 'object' && !Array.isArray(reader.colorLabels) ? reader.colorLabels : {};
|
||
const ink = reader.ink && typeof reader.ink === 'object' && !Array.isArray(reader.ink) ? reader.ink : {};
|
||
const plans = data.readingPlans && typeof data.readingPlans === 'object' ? data.readingPlans : {};
|
||
const prayers = Array.isArray(data.prayers) ? data.prayers.filter((p) => p && typeof p.id === 'string' && typeof p.text === 'string' && p.text.trim() && !p.deleted) : [];
|
||
const backup = {
|
||
projects, prayers, reader: { bookmarks, bookmarkNotes, highlights, colorLabels, ink },
|
||
readingPlans: {
|
||
progress: Array.isArray(plans.progress) ? plans.progress : [],
|
||
customPlans: Array.isArray(plans.customPlans) ? plans.customPlans : [],
|
||
activity: Array.isArray(plans.activity) ? plans.activity : [],
|
||
},
|
||
};
|
||
return {
|
||
ok: true, backup,
|
||
summary: {
|
||
projects: projects.length, bookmarks: Object.keys(bookmarks).length, highlights: highlights.length,
|
||
inkPages: Object.keys(ink).length, prayers: prayers.length, plans: backup.readingPlans.progress.length + backup.readingPlans.customPlans.length,
|
||
},
|
||
};
|
||
}
|
||
|
||
// Brings the studies back onto this device: a study that isn't here is added, one that is here is
|
||
// replaced only when the backup's copy was edited more recently. -> the projects that were written
|
||
// (so the caller can send them to the server too).
|
||
export function restoreProjects(projects) {
|
||
const written = [];
|
||
for (const project of projects) {
|
||
const here = loadProjectById(project.id);
|
||
if (here && (here.lastEdited ?? 0) >= (project.lastEdited ?? 0)) continue;
|
||
written.push(saveProjectToStorage(project));
|
||
}
|
||
return written;
|
||
}
|
||
|
||
// Reading-plan progress from a backup combined with what is here: each plan run is kept once, and the
|
||
// copy with the later activity wins; custom plans and activity days are unioned.
|
||
export function mergeReadingPlans(local, incoming) {
|
||
const progress = new Map((local.progress ?? []).map((p) => [p.id, p]));
|
||
for (const p of incoming.progress ?? []) {
|
||
const mine = progress.get(p.id);
|
||
if (!mine || (p.lastActivityAt ?? 0) > (mine.lastActivityAt ?? 0)) progress.set(p.id, p);
|
||
}
|
||
const customPlans = new Map((local.customPlans ?? []).map((p) => [p.id, p]));
|
||
for (const p of incoming.customPlans ?? []) if (!customPlans.has(p.id)) customPlans.set(p.id, p);
|
||
return {
|
||
progress: [...progress.values()],
|
||
customPlans: [...customPlans.values()],
|
||
activity: [...new Set([...(local.activity ?? []), ...(incoming.activity ?? [])])].sort(),
|
||
};
|
||
}
|
||
|
||
// When you last downloaded a backup on this device, so Home can say so (and nudge when it has been a while).
|
||
const LAST_BACKUP_KEY = 'last-backup-at';
|
||
export const markBackedUp = (now = Date.now()) => { try { localStorage.setItem(LAST_BACKUP_KEY, String(now)); } catch { /* storage unavailable */ } };
|
||
export const lastBackupAt = () => { try { return Number(localStorage.getItem(LAST_BACKUP_KEY)) || null; } catch { return null; } };
|
||
export function backupAge(at, now = Date.now()) {
|
||
if (!at) return { text: 'You haven’t downloaded a backup yet', stale: true };
|
||
const days = Math.floor((now - at) / 86400000);
|
||
if (days < 1) return { text: 'Backed up today', stale: false };
|
||
return { text: `Last backup ${days} day${days === 1 ? '' : 's'} ago`, stale: days >= 14 };
|
||
}
|