// "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 }; }