Build and Push Docker Image / build-and-push (push) Failing after 19s
Bookmarks and text highlights are saved per user and merged across devices (union on first contact, newer server copy when this device is clean). Theme, default translation, verse layout, commentary source and reading position sync too, each with its own timestamp so the newest change to each setting wins. The Reader never moves under you while it is open. Signing out clears this device's marks and forgets sync timestamps. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
46 lines
1.9 KiB
JavaScript
46 lines
1.9 KiB
JavaScript
// A user's synced display preferences: theme, default Reader translation, verse layout, the
|
|
// commentary they last chose, and where they left off in the Reader. Each preference carries
|
|
// its own timestamp ({ v, at }) so the most recent change to each one wins, device by device.
|
|
const ENUMS = {
|
|
theme: ['paper', 'sepia', 'dark'],
|
|
translation: ['BSB', 'ESV', 'NLT', 'CSB', 'NASB', 'NET'],
|
|
layout: ['flow', 'verses'],
|
|
commentarySource: ['matthew-henry', 'john-gill', 'jamieson-fausset-brown', 'tyndale', 'adam-clarke', 'john-calvin', 'keil-delitzsch'],
|
|
};
|
|
const DAY_MS = 86_400_000;
|
|
|
|
// A cleaned value, or undefined when it isn't acceptable for that preference.
|
|
export function cleanPrefValue(key, value) {
|
|
if (ENUMS[key]) return ENUMS[key].includes(value) ? value : undefined;
|
|
if (key === 'position') {
|
|
if (!value || typeof value !== 'object') return undefined;
|
|
const book = String(value.book ?? '').toUpperCase();
|
|
if (!/^[1-3A-Z]{3}$/.test(book) || !Number.isInteger(value.chapter) || value.chapter < 1 || value.chapter > 200) return undefined;
|
|
return { book, chapter: value.chapter };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// Body -> { key: { v, at } } with only known keys, valid values and sane timestamps.
|
|
export function cleanPrefsPatch(body, now = Date.now()) {
|
|
const out = {};
|
|
const prefs = body?.prefs;
|
|
if (!prefs || typeof prefs !== 'object' || Array.isArray(prefs)) return out;
|
|
for (const [key, entry] of Object.entries(prefs)) {
|
|
const v = cleanPrefValue(key, entry?.v);
|
|
const at = entry?.at;
|
|
if (v === undefined || !Number.isFinite(at) || at < 0 || at > now + DAY_MS) continue;
|
|
out[key] = { v, at };
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Per key, the entry with the newer timestamp wins (the stored one on a tie).
|
|
export function mergePrefs(existing = {}, patch = {}) {
|
|
const merged = { ...existing };
|
|
for (const [key, entry] of Object.entries(patch)) {
|
|
if (!merged[key] || entry.at > merged[key].at) merged[key] = entry;
|
|
}
|
|
return merged;
|
|
}
|