App quality: offline badge, per-note history, keyboard study mode, notes import
- Study cards say whether the Bible text works offline, with a button to save the missing book (notes are always kept on the device). - Each note keeps its last 12 versions with Restore (History under each note). - Keyboard-only study: 1/2/3 write in Observe/Interpret/Apply, C/W/X/P switch resource tabs, V compares, Esc leaves a note box; listed under "?". - Export > Import notes: bring Word/Markdown/text notes into the matching chunks without overwriting anything; can mark them as not your own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
648c3206df
commit
67645cf1df
14 files changed
+421
-7
No files matched your search
@@ -1,6 +1,7 @@
|
||||
// The Export stage: take the finished study out as a file, or share it read-only.
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { useShareLink } from '../hooks/useShareLink.js';
|
||||
import ImportNotes from '../components/ImportNotes.jsx';
|
||||
import HandoutExport from '../components/HandoutExport.jsx';
|
||||
import SessionSchedule from '../components/SessionSchedule.jsx';
|
||||
import VersionHistory from '../components/VersionHistory.jsx';
|
||||
@@ -63,6 +64,8 @@ export default function ExportPage() {
|
||||
|
||||
<HandoutExport project={project} />
|
||||
|
||||
<ImportNotes />
|
||||
|
||||
<SessionSchedule project={project} />
|
||||
|
||||
<CoTeachers projectId={project?.id} sharedBy={project && sharedStudies?.[project.id]} onLeave={() => { deleteProject(project.id); setCurrentPage('home'); }} />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { filterStudies, groupBySeries, isTagQuery, seriesList, tagCounts } from '../utils/studyFilter.js';
|
||||
import BookCover from '../components/BookCover.jsx';
|
||||
import WeeklyRecall from '../components/WeeklyRecall.jsx';
|
||||
import OfflineBadge from '../components/OfflineBadge.jsx';
|
||||
import { useOfflineSave } from '../hooks/useOfflineSave.js';
|
||||
import RecallHistory from '../components/RecallHistory.jsx';
|
||||
import IqDailyCard from '../components/iq/IqDailyCard.jsx';
|
||||
import IqNarratorOptions, { useLazyNarrators } from '../components/iq/IqNarratorOptions.jsx';
|
||||
@@ -55,6 +57,9 @@ export default function HomePage() {
|
||||
handleSeekAudioTo,
|
||||
} = useApp();
|
||||
const narrators = useLazyNarrators(audioNarrator);
|
||||
const offline = useOfflineSave();
|
||||
// Each study's books and translation, read once per list change (the badge itself follows what's saved).
|
||||
const studyBooks = useMemo(() => Object.fromEntries(projectIndex.map((e) => [e.id, loadProjectById(e.id)])), [projectIndex]);
|
||||
const [homeSearch, setHomeSearch] = useState('');
|
||||
const [homeSort, setHomeSort] = useState(() => {
|
||||
try { return localStorage.getItem('homeSort') || 'recent'; } catch { return 'recent'; }
|
||||
@@ -320,6 +325,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-[var(--r-sub)]">{formatRelativeDate(entry.lastEdited)}</p>
|
||||
<OfflineBadge project={studyBooks[entry.id]} offline={offline} />
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 ${homeView === 'list' ? 'sm:w-56 sm:shrink-0' : ''}`}>
|
||||
<button type="button" onClick={() => resumeProject(entry.id)} className="flex-1 rounded-xl bg-[var(--r-text)] px-4 py-2 text-sm font-medium text-[var(--r-bg)] hover:opacity-90">Resume</button>
|
||||
|
||||
+17
-2
@@ -16,6 +16,10 @@ import ResourcePane, { useResourceTab } from '../components/study/ResourcePane.j
|
||||
import CompareSheet from '../components/study/CompareSheet.jsx';
|
||||
import SuggestWordsSheet from '../components/study/SuggestWordsSheet.jsx';
|
||||
|
||||
// Keyboard-only study: 1 / 2 / 3 jump to Observe / Interpret / Apply; c, w, x, p open the resource tabs; v compares.
|
||||
const NOTE_KEYS = { 1: 'observation', 2: 'interpretation', 3: 'application' };
|
||||
const TAB_KEYS = { c: 'commentary', w: 'words', x: 'refs', p: 'people' };
|
||||
|
||||
export default function StudyPage() {
|
||||
const { project, allChunks, projectIndex, updateProject, updateChunk, setCurrentPage, readerCommentary, setReaderCommentary, saveUserTemplate, startPrayerFor } = useApp();
|
||||
|
||||
@@ -47,6 +51,13 @@ export default function StudyPage() {
|
||||
const [folded, setFolded] = useState(() => {
|
||||
try { const f = JSON.parse(localStorage.getItem('study-folded')); return { notes: !!f?.notes, resources: !!f?.resources && !f?.notes }; } catch { return { notes: false, resources: false }; }
|
||||
});
|
||||
const [focusRequest, setFocusRequest] = useState(null);
|
||||
const showHalf = (which) => setFolded((cur) => {
|
||||
if (!cur[which]) return cur;
|
||||
const next = { ...cur, [which]: false };
|
||||
try { localStorage.setItem('study-folded', JSON.stringify(next)); } catch { /* storage unavailable */ }
|
||||
return next;
|
||||
});
|
||||
const toggleFold = (which) => setFolded((cur) => {
|
||||
const other = which === 'notes' ? 'resources' : 'notes';
|
||||
// folding the last open half opens the other one instead, so the column is never empty
|
||||
@@ -76,11 +87,15 @@ export default function StudyPage() {
|
||||
const editable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable;
|
||||
if (event.key === 'Escape') {
|
||||
if (wordStudy.suggestModal) { wordStudy.setSuggestModal(null); wordStudy.setSuggestSelection(new Set()); }
|
||||
else if (editable && !comparing) event.target.blur(); // leave a note box and go back to keyboard moves
|
||||
return;
|
||||
}
|
||||
if (editable || wordStudy.suggestModal) return;
|
||||
if (editable || wordStudy.suggestModal || comparing || event.metaKey || event.ctrlKey || event.altKey) return;
|
||||
if (event.key === 'ArrowLeft') { event.preventDefault(); goPrev(); }
|
||||
else if (event.key === 'ArrowRight') { event.preventDefault(); goNext(); }
|
||||
else if (NOTE_KEYS[event.key]) { event.preventDefault(); showHalf('notes'); setFocusRequest({ field: NOTE_KEYS[event.key], n: Date.now() }); }
|
||||
else if (TAB_KEYS[event.key.toLowerCase()]) { event.preventDefault(); showHalf('resources'); setTab(TAB_KEYS[event.key.toLowerCase()]); }
|
||||
else if (event.key.toLowerCase() === 'v') { event.preventDefault(); setComparing(true); }
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
@@ -154,7 +169,7 @@ export default function StudyPage() {
|
||||
</button>
|
||||
</h2>
|
||||
<div className={`min-h-0 flex-1 overflow-y-auto border-t border-[var(--r-line)] p-3 ${folded.notes ? 'hidden' : ''}`}>
|
||||
<NotesPane chunk={selectedChunk} updateChunk={updateChunk} allTags={allTags} templateId={project?.templateDef ?? project?.templateId} saveTemplate={saveUserTemplate} notify={notify}
|
||||
<NotesPane chunk={selectedChunk} updateChunk={updateChunk} allTags={allTags} templateId={project?.templateDef ?? project?.templateId} saveTemplate={saveUserTemplate} notify={notify} focusRequest={focusRequest}
|
||||
onPray={() => startPrayerFor(formatChunkReference(project, chapterIndex, selectedChunk, '–'))} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user