// Reading data synced between devices: ink, preferences, bookmarks and highlights, prayers, reading history, plan progress. import { getAllReaderInk, getPrayers, getReaderHistory, getReaderMarks, getReadingProgress, getUserPrefs, setPrayers, setReaderHistory, setReaderInkPage, setReaderMarks, setReadingProgress, setUserPrefs } from '../db.js'; import { requireAuth } from '../auth.js'; import { mergePrayers } from '../prayers.js'; import { mergeHistory } from '../readerHistory.js'; import { cleanMarks } from '../readerMarks.js'; import { cleanPrefsPatch, mergePrefs } from '../prefs.js'; export function registerReaderRoutes(app) { // --------------------------------------------------------------------------- // Reader ink — cross-device draw annotations on Bible chapters // --------------------------------------------------------------------------- // GET /api/reader/ink — all saved ink pages for the current user app.get('/api/reader/ink', requireAuth, (req, res) => { try { res.json(getAllReaderInk(req.session.userId)); } catch (err) { console.error('GET /api/reader/ink error:', err); res.status(500).json({ error: 'Failed to load reader ink.' }); } }); // PUT /api/reader/ink/:book/:chapter — save (upsert) one page's ink app.put('/api/reader/ink/:book/:chapter', requireAuth, (req, res) => { try { const { book, chapter } = req.params; const { strokes } = req.body ?? {}; if (!/^[1-3A-Za-z]{3}$/.test(book) || !/^\d{1,3}$/.test(chapter)) return res.status(400).json({ error: 'Invalid book or chapter.' }); if (!Array.isArray(strokes)) return res.status(400).json({ error: 'strokes must be an array.' }); if (strokes.length > 20000) return res.status(413).json({ error: 'Too much ink on one page.' }); setReaderInkPage(req.session.userId, book, chapter, strokes); res.json({ ok: true }); } catch (err) { console.error('PUT /api/reader/ink error:', err); res.status(500).json({ error: 'Failed to save reader ink.' }); } }); // GET /api/prefs — the user's synced display preferences ({ prefs: { key: { v, at } } }) app.get('/api/prefs', requireAuth, (req, res) => { try { res.json({ prefs: getUserPrefs(req.session.userId) }); } catch (err) { console.error('GET /api/prefs error:', err); res.status(500).json({ error: 'Failed to load preferences.' }); } }); // PUT /api/prefs — merge changed preferences in (newest change per key wins); returns the result app.put('/api/prefs', requireAuth, (req, res) => { try { const merged = mergePrefs(getUserPrefs(req.session.userId), cleanPrefsPatch(req.body)); setUserPrefs(req.session.userId, merged); res.json({ prefs: merged }); } catch (err) { console.error('PUT /api/prefs error:', err); res.status(500).json({ error: 'Failed to save preferences.' }); } }); // GET /api/reader/marks — the user's bookmarks + highlights, with an updatedAt to compare against app.get('/api/reader/marks', requireAuth, (req, res) => { try { res.json(getReaderMarks(req.session.userId)); } catch (err) { console.error('GET /api/reader/marks error:', err); res.status(500).json({ error: 'Failed to load bookmarks and highlights.' }); } }); // GET /api/prayers — the prayer journal; PUT merges this device's copy in (newer prayer wins) and returns the whole app.get('/api/prayers', requireAuth, (req, res) => { try { res.json({ prayers: mergePrayers(getPrayers(req.session.userId), []) }); } catch (err) { console.error('GET /api/prayers error:', err); res.status(500).json({ error: 'Failed to load your prayers.' }); } }); app.put('/api/prayers', requireAuth, (req, res) => { try { const merged = mergePrayers(getPrayers(req.session.userId), req.body?.prayers); setPrayers(req.session.userId, merged); res.json({ prayers: merged }); } catch (err) { console.error('PUT /api/prayers error:', err); res.status(500).json({ error: 'Failed to save your prayers.' }); } }); // GET /api/reader/history — what has been read, and PUT merges this device's copy in and returns the whole app.get('/api/reader/history', requireAuth, (req, res) => { try { res.json(mergeHistory(getReaderHistory(req.session.userId), {})); } catch (err) { console.error('GET /api/reader/history error:', err); res.status(500).json({ error: 'Failed to load your reading history.' }); } }); app.put('/api/reader/history', requireAuth, (req, res) => { try { const merged = mergeHistory(getReaderHistory(req.session.userId), req.body); setReaderHistory(req.session.userId, merged); res.json(merged); } catch (err) { console.error('PUT /api/reader/history error:', err); res.status(500).json({ error: 'Failed to save your reading history.' }); } }); // PUT /api/reader/marks — replace the user's bookmarks + highlights ({ bookmarks, highlights }) app.put('/api/reader/marks', requireAuth, (req, res) => { try { const marks = cleanMarks(req.body); if (!marks) return res.status(400).json({ error: 'Body must be an object.' }); res.json({ ok: true, updatedAt: setReaderMarks(req.session.userId, marks) }); } catch (err) { console.error('PUT /api/reader/marks error:', err); res.status(500).json({ error: 'Failed to save bookmarks and highlights.' }); } }); // --------------------------------------------------------------------------- // Reading plan progress — cross-device sync for started/completed reading plans // --------------------------------------------------------------------------- // GET /api/reading-plans — the current user's whole progress list app.get('/api/reading-plans', requireAuth, (req, res) => { try { res.json(getReadingProgress(req.session.userId)); } catch (err) { console.error('GET /api/reading-plans error:', err); res.status(500).json({ error: 'Failed to load reading plan progress.' }); } }); // PUT /api/reading-plans — overwrite the current user's whole progress list // (an object: { progress: [...], customPlans: [...] }) app.put('/api/reading-plans', requireAuth, (req, res) => { try { if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) { return res.status(400).json({ error: 'Body must be an object.' }); } const { progress, customPlans, activity } = req.body; if ([progress, customPlans, activity].some((v) => v !== undefined && !Array.isArray(v)) || (progress?.length ?? 0) > 500 || (customPlans?.length ?? 0) > 200 || (activity?.length ?? 0) > 20000) { return res.status(400).json({ error: 'progress, customPlans and activity must be reasonably sized lists.' }); } setReadingProgress(req.session.userId, { progress: progress ?? [], customPlans: customPlans ?? [], activity: activity ?? [] }); res.json({ ok: true }); } catch (err) { console.error('PUT /api/reading-plans error:', err); res.status(500).json({ error: 'Failed to save reading plan progress.' }); } }); }