Study - New Outline stage (hierarchical I./A./1. points, linked to chunks, in all exports); the chunk stage is now "Chunks" - Home cards: passages shown as ranges, tags capped with "+N more" Reader - Side-by-side translations and a Compare tab per verse - Typeface, line spacing, verse-number settings; text and column scale with the screen - Highlights/bookmarks sheet with named colours (synced); BSB footnote spacing fix - Cmd-K palette (passages, studies, notes, marks), keyboard shortcuts and "?" help Flow and safety - Toasts with Undo replace most confirm()/alert() - Back/Forward move between pages - Server refuses saves over a newer copy (409); keep-mine / use-other banner; opening a study no longer re-stamps it - Latest chapter request wins when flipping quickly Offline and data - Save books, plan days or a whole BSB/NET for offline - Service worker fixes: helloao /api/ URLs were skipped and responses cloned too late - Settings: download/restore a full backup Accessibility - Sheets trap focus and close on Escape, skip link, focus ring, reduced motion, theme contrast raised to 4.5:1 (with a test) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
71 lines
3.2 KiB
JavaScript
71 lines
3.2 KiB
JavaScript
// Search across everything the reader has written or saved on this device: study notes, project
|
||
// titles and passages, bookmarks and highlights. (Bible text itself is searched in the Reader.)
|
||
import { bookOptions, formatChunkReference } from './bibleUtils.jsx';
|
||
|
||
const NOTE_FIELDS = [
|
||
['observation', 'Observation'], ['interpretation', 'Interpretation'], ['application', 'Application'],
|
||
['generalNotes', 'Notes'], ['finalScript', 'Script'],
|
||
];
|
||
|
||
const snippetAround = (raw, at, len) => {
|
||
const start = Math.max(0, at - 40);
|
||
const end = at + len + 60;
|
||
return (start > 0 ? '…' : '') + raw.slice(start, end).replace(/\s+/g, ' ') + (end < raw.length ? '…' : '');
|
||
};
|
||
|
||
// projects: full project objects. -> [{ projectId, projectTitle, chunkId, ref, field, snippet }], at most
|
||
// `limit`, one hit per chunk (the first field that matches).
|
||
export function searchNotes(projects, query, { limit = 20 } = {}) {
|
||
const q = query.trim().toLowerCase();
|
||
if (q.length < 3) return [];
|
||
const hits = [];
|
||
for (const project of projects) {
|
||
for (const [chapterIndex, chapter] of (project.chapters ?? []).entries()) {
|
||
for (const chunk of chapter.chunks ?? []) {
|
||
for (const [field, label] of NOTE_FIELDS) {
|
||
const raw = chunk[field] ?? '';
|
||
const at = raw.toLowerCase().indexOf(q);
|
||
if (at === -1) continue;
|
||
hits.push({
|
||
projectId: project.id, projectTitle: project.title, chunkId: chunk.id,
|
||
ref: formatChunkReference(project, chapterIndex, chunk, '–'), field: label, snippet: snippetAround(raw, at, q.length),
|
||
});
|
||
break;
|
||
}
|
||
if (hits.length >= limit) return hits;
|
||
}
|
||
}
|
||
}
|
||
return hits;
|
||
}
|
||
|
||
// Projects whose title, passages or tags contain the text.
|
||
export function searchProjects(index, query, { limit = 6 } = {}) {
|
||
const q = query.trim().toLowerCase();
|
||
if (!q) return [];
|
||
return index.filter((e) => [e.title, e.chapterSummary, ...(e.tags ?? [])].some((t) => (t ?? '').toLowerCase().includes(q))).slice(0, limit);
|
||
}
|
||
|
||
const bookName = (abbrev) => bookOptions.find((b) => b.abbrev === abbrev)?.name ?? abbrev;
|
||
|
||
// Highlights (by their words or what the colour is named) and bookmarks (by reference).
|
||
// -> [{ kind: 'highlight' | 'bookmark', book, bookName, chapter, verse, color, text?, label? }]
|
||
export function searchMarks({ bookmarks = {}, highlights = [], labels = {} }, query, { limit = 10 } = {}) {
|
||
const q = query.trim().toLowerCase();
|
||
if (q.length < 2) return [];
|
||
const out = [];
|
||
for (const h of highlights) {
|
||
const label = labels[h.color] ?? '';
|
||
const ref = `${bookName(h.book)} ${h.chapter}:${h.verse}`.toLowerCase();
|
||
if ((h.text ?? '').toLowerCase().includes(q) || label.toLowerCase().includes(q) || ref.includes(q)) {
|
||
out.push({ kind: 'highlight', book: h.book, bookName: bookName(h.book), chapter: h.chapter, verse: h.verse, color: h.color, text: h.text, label });
|
||
}
|
||
}
|
||
for (const [key, color] of Object.entries(bookmarks)) {
|
||
const [book, chapter, verse] = key.split('-');
|
||
const ref = `${bookName(book)} ${chapter}:${verse}`;
|
||
if (ref.toLowerCase().includes(q)) out.push({ kind: 'bookmark', book, bookName: bookName(book), chapter: Number(chapter), verse: Number(verse), color });
|
||
}
|
||
return out.slice(0, limit);
|
||
}
|