Move chunk builder into SetupPage via useChunkBuilder
- The setup form, active chapter tab, verse range selection, typed/bulk/ cross-chapter chunk creation, reorder, and delete move out of App. - Setup now opens on the chapter holding the chunk last studied (e.g. after Back to Chunks), and the Add Chapter form suggests the chapter after the project's last one instead of repeating the current one. - openNewProject/goHome/resume/import no longer reset setup state by hand; SetupPage starts fresh each time it opens. - Tests: add-chapter default, Back to Chunks chapter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf02e071fa
commit
0f1a9c3d5b
6 files changed
+555
-518
No files matched your search
+1
-510
@@ -9,10 +9,7 @@ import { useSharedView } from './hooks/useSharedView.js';
|
||||
import HomePage from './pages/HomePage.jsx';
|
||||
|
||||
import {
|
||||
bookOptions,
|
||||
makeId,
|
||||
buildExportHtml,
|
||||
parseBibleChapter,
|
||||
loadProjectIndex,
|
||||
switchStorageUser,
|
||||
} from './utils/bibleUtils.jsx';
|
||||
@@ -45,31 +42,11 @@ const PronunciationPage = lazy(() => import('./pages/PronunciationPage.jsx'));
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const App = () => {
|
||||
const [setup, setSetup] = useState({
|
||||
translation: 'BSB',
|
||||
book: 'Titus',
|
||||
bookAbbrev: 'TIT',
|
||||
chapter: '1',
|
||||
title: 'Titus 1 Study',
|
||||
});
|
||||
const [titleEdited, setTitleEdited] = useState(false);
|
||||
const [project, setProject] = useState(null);
|
||||
// 'home' | 'setup' | 'study' | 'settings'
|
||||
const [currentPage, setCurrentPage] = useState('home');
|
||||
const [projectIndex, setProjectIndex] = useState([]);
|
||||
const [activeChapterIndex, setActiveChapterIndex] = useState(0);
|
||||
const [showAddChapterForm, setShowAddChapterForm] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState('');
|
||||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [rangeEnd, setRangeEnd] = useState(null);
|
||||
const [loadingChapter, setLoadingChapter] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [typedChunkStart, setTypedChunkStart] = useState('');
|
||||
const [typedChunkEnd, setTypedChunkEnd] = useState('');
|
||||
const [typedChunkNextEnd, setTypedChunkNextEnd] = useState('');
|
||||
const [clickedSpanNextEnd, setClickedSpanNextEnd] = useState('');
|
||||
const [typedChunkBulk, setTypedChunkBulk] = useState('');
|
||||
const saveTimerRef = useRef(null);
|
||||
const [syncStatus, setSyncStatus] = useState(''); // '' | 'syncing' | 'synced' | 'error'
|
||||
const reader = useBibleReader(currentPage);
|
||||
@@ -95,9 +72,7 @@ const App = () => {
|
||||
setProject,
|
||||
setProjectIndex,
|
||||
setCurrentPage,
|
||||
setActiveChapterIndex,
|
||||
});
|
||||
const [verseSearch, setVerseSearch] = useState('');
|
||||
const openBibleReader = () => {
|
||||
setCurrentPage('reader');
|
||||
};
|
||||
@@ -110,17 +85,7 @@ const App = () => {
|
||||
setProjectIndex(loadProjectIndex());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!titleEdited) {
|
||||
setSetup((current) => ({
|
||||
...current,
|
||||
title: `${current.book} ${current.chapter} Study`,
|
||||
}));
|
||||
}
|
||||
}, [setup.book, setup.chapter, titleEdited]);
|
||||
|
||||
const allChunks = project ? project.chapters.flatMap((ch) => ch.chunks) : [];
|
||||
const activeChapter = project?.chapters[activeChapterIndex] ?? null;
|
||||
// Autosave
|
||||
useEffect(() => {
|
||||
if (!project) return;
|
||||
@@ -147,412 +112,10 @@ const App = () => {
|
||||
};
|
||||
}, [project]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handleSetupField = (field, value) => {
|
||||
if (field === 'book') {
|
||||
const selected = bookOptions.find((book) => book.abbrev === value) ?? bookOptions[0];
|
||||
setSetup((current) => ({
|
||||
...current,
|
||||
book: selected.name,
|
||||
bookAbbrev: selected.abbrev,
|
||||
}));
|
||||
} else {
|
||||
setSetup((current) => ({ ...current, [field]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadChapter = async () => {
|
||||
if (!setup.bookAbbrev || !setup.chapter) {
|
||||
setErrorMessage('Please choose a book and chapter.');
|
||||
return;
|
||||
}
|
||||
setErrorMessage('');
|
||||
setLoadingChapter(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://bible.helloao.org/api/${setup.translation}/${setup.bookAbbrev}/${setup.chapter}.json`,
|
||||
);
|
||||
if (!response.ok) throw new Error('Unable to load chapter.');
|
||||
const data = await response.json();
|
||||
const verses = parseBibleChapter(data);
|
||||
if (!data || !Array.isArray(verses) || verses.length === 0) {
|
||||
throw new Error('Invalid Bible data returned.');
|
||||
}
|
||||
|
||||
if (project) {
|
||||
// Adding a chapter to an existing project
|
||||
const alreadyExists = project.chapters.some(
|
||||
(ch) => ch.bookAbbrev === setup.bookAbbrev && ch.chapter === setup.chapter,
|
||||
);
|
||||
if (alreadyExists) {
|
||||
setErrorMessage('That chapter is already in this project.');
|
||||
setLoadingChapter(false);
|
||||
return;
|
||||
}
|
||||
const newChapter = {
|
||||
book: setup.book,
|
||||
bookAbbrev: setup.bookAbbrev,
|
||||
chapter: setup.chapter,
|
||||
verses,
|
||||
chunks: [],
|
||||
};
|
||||
updateProject((current) => {
|
||||
const updatedChapters = [...current.chapters, newChapter];
|
||||
return { ...current, chapters: updatedChapters };
|
||||
});
|
||||
setActiveChapterIndex(project.chapters.length);
|
||||
setShowAddChapterForm(false);
|
||||
setErrorMessage('');
|
||||
} else {
|
||||
// Creating a brand-new project
|
||||
const newProject = {
|
||||
id: makeId(),
|
||||
title: setup.title,
|
||||
translation: setup.translation,
|
||||
lastEdited: Date.now(),
|
||||
selectedChunkId: null,
|
||||
chapters: [
|
||||
{
|
||||
book: setup.book,
|
||||
bookAbbrev: setup.bookAbbrev,
|
||||
chapter: setup.chapter,
|
||||
verses,
|
||||
chunks: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
setProject(newProject);
|
||||
setActiveChapterIndex(0);
|
||||
setCurrentPage('setup');
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Failed to load chapter.');
|
||||
} finally {
|
||||
setLoadingChapter(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProject = (updater) => {
|
||||
setProject((current) => (current ? updater(current) : current));
|
||||
};
|
||||
|
||||
const beginStudying = () => {
|
||||
if (!project || allChunks.length === 0) return;
|
||||
updateProject((current) => ({
|
||||
...current,
|
||||
selectedChunkId: current.selectedChunkId || allChunks[0]?.id || null,
|
||||
}));
|
||||
setCurrentPage('study');
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chunk CRUD (operates on activeChapter in setup view, on selectedChunkChapter in study)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const addChunkToChapter = (chapterIndex, start, end, spilloverEndVerse = null) => {
|
||||
updateProject((current) => {
|
||||
const chapters = current.chapters.map((ch, idx) => {
|
||||
if (idx !== chapterIndex) return ch;
|
||||
const overlap = ch.chunks.find(
|
||||
(c) => c.startVerse === start
|
||||
&& c.endVerse === end
|
||||
&& (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
|
||||
);
|
||||
if (overlap) return ch;
|
||||
const newChunk = {
|
||||
id: makeId(),
|
||||
startVerse: start,
|
||||
endVerse: end,
|
||||
spilloverEndVerse,
|
||||
observation: '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
crossReferences: [],
|
||||
greekWords: [],
|
||||
generalNotes: '',
|
||||
episodeNumber: '',
|
||||
episodeTitle: '',
|
||||
finalScript: '',
|
||||
tags: [],
|
||||
};
|
||||
return { ...ch, chunks: [...ch.chunks, newChunk] };
|
||||
});
|
||||
// Find the newly added chunk id
|
||||
const newChunk = chapters[chapterIndex].chunks.at(-1);
|
||||
return { ...current, chapters, selectedChunkId: newChunk?.id ?? current.selectedChunkId };
|
||||
});
|
||||
};
|
||||
|
||||
const addChunk = (start, end, spilloverEndVerse = null) => {
|
||||
if (!project) return;
|
||||
const chapter = activeChapter;
|
||||
if (!chapter) return;
|
||||
const overlap = chapter.chunks.find(
|
||||
(c) => c.startVerse === start
|
||||
&& c.endVerse === end
|
||||
&& (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
|
||||
);
|
||||
if (overlap) {
|
||||
setStatusMessage('That chunk already exists.');
|
||||
window.setTimeout(() => setStatusMessage(''), 1800);
|
||||
return;
|
||||
}
|
||||
addChunkToChapter(activeChapterIndex, start, end, spilloverEndVerse);
|
||||
};
|
||||
|
||||
const addChunkRanges = (ranges) => {
|
||||
if (!project || !activeChapter || ranges.length === 0) {
|
||||
return { addedCount: 0, skippedCount: 0 };
|
||||
}
|
||||
|
||||
let addedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let lastAddedChunkId = null;
|
||||
|
||||
updateProject((current) => {
|
||||
const chapters = current.chapters.map((ch, idx) => {
|
||||
if (idx !== activeChapterIndex) return ch;
|
||||
const existing = new Set(
|
||||
ch.chunks.map((c) => `${c.startVerse}-${c.endVerse}-${c.spilloverEndVerse ?? ''}`),
|
||||
);
|
||||
const newChunks = [];
|
||||
|
||||
ranges.forEach(({ start, end, spilloverEndVerse = null }) => {
|
||||
const key = `${start}-${end}-${spilloverEndVerse ?? ''}`;
|
||||
if (existing.has(key)) {
|
||||
skippedCount += 1;
|
||||
return;
|
||||
}
|
||||
const newChunk = {
|
||||
id: makeId(),
|
||||
startVerse: start,
|
||||
endVerse: end,
|
||||
spilloverEndVerse,
|
||||
observation: '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
crossReferences: [],
|
||||
greekWords: [],
|
||||
tags: [],
|
||||
};
|
||||
existing.add(key);
|
||||
newChunks.push(newChunk);
|
||||
addedCount += 1;
|
||||
lastAddedChunkId = newChunk.id;
|
||||
});
|
||||
|
||||
return newChunks.length > 0
|
||||
? { ...ch, chunks: [...ch.chunks, ...newChunks] }
|
||||
: ch;
|
||||
});
|
||||
|
||||
return {
|
||||
...current,
|
||||
chapters,
|
||||
selectedChunkId: lastAddedChunkId ?? current.selectedChunkId,
|
||||
};
|
||||
});
|
||||
|
||||
return { addedCount, skippedCount };
|
||||
};
|
||||
|
||||
const addTypedChunk = () => {
|
||||
if (!activeChapter) return;
|
||||
const start = Number.parseInt(typedChunkStart, 10);
|
||||
const end = Number.parseInt(typedChunkEnd, 10);
|
||||
const nextEndRaw = typedChunkNextEnd.trim();
|
||||
const nextEnd = nextEndRaw ? Number.parseInt(nextEndRaw, 10) : null;
|
||||
const maxVerse = activeChapter.verses.at(-1)?.number ?? 0;
|
||||
const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
|
||||
const nextMaxVerse = nextChapter?.verses?.at(-1)?.number ?? 0;
|
||||
|
||||
if (!Number.isInteger(start) || !Number.isInteger(end)) {
|
||||
setStatusMessage('Type a valid start and end verse.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2200);
|
||||
return;
|
||||
}
|
||||
if (start < 1 || end < 1 || start > maxVerse || end > maxVerse) {
|
||||
setStatusMessage(`Verse range must be between 1 and ${maxVerse}.`);
|
||||
window.setTimeout(() => setStatusMessage(''), 2200);
|
||||
return;
|
||||
}
|
||||
if (start > end) {
|
||||
setStatusMessage('Start verse must be less than or equal to end verse.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextEndRaw) {
|
||||
if (!nextChapter) {
|
||||
setStatusMessage('Add the next chapter first to span chunks across chapters.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (nextChapter.bookAbbrev !== activeChapter.bookAbbrev) {
|
||||
setStatusMessage('Chapter spanning only works into the next chapter of the same book.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(nextEnd) || nextEnd < 1 || nextEnd > nextMaxVerse) {
|
||||
setStatusMessage(`Next chapter end must be between 1 and ${nextMaxVerse}.`);
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (end !== maxVerse) {
|
||||
setStatusMessage('To span chapters, set End to the last verse of this chapter.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { addedCount } = addChunkRanges([{ start, end, spilloverEndVerse: nextEnd }]);
|
||||
if (addedCount === 0) {
|
||||
setStatusMessage('That chunk already exists.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
setTypedChunkStart('');
|
||||
setTypedChunkEnd('');
|
||||
setTypedChunkNextEnd('');
|
||||
};
|
||||
|
||||
const addBulkTypedChunks = () => {
|
||||
if (!activeChapter) return;
|
||||
const raw = typedChunkBulk.trim();
|
||||
if (!raw) {
|
||||
setStatusMessage('Type chunk ranges first. Example: 1-6, 7-12');
|
||||
window.setTimeout(() => setStatusMessage(''), 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
const maxVerse = activeChapter.verses.at(-1)?.number ?? 0;
|
||||
const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
|
||||
const nextMaxVerse = nextChapter?.verses?.at(-1)?.number ?? 0;
|
||||
const parts = raw
|
||||
.split(/[\n,;]+/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const validRanges = [];
|
||||
let invalidCount = 0;
|
||||
|
||||
parts.forEach((part) => {
|
||||
const single = /^(\d+)$/.exec(part);
|
||||
if (single) {
|
||||
const v = Number.parseInt(single[1], 10);
|
||||
if (v >= 1 && v <= maxVerse) {
|
||||
validRanges.push({ start: v, end: v });
|
||||
} else {
|
||||
invalidCount += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const range = /^(\d+)\s*-\s*(\d+)(?:\s*:\s*(\d+))?$/.exec(part);
|
||||
if (!range) {
|
||||
invalidCount += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Number.parseInt(range[1], 10);
|
||||
const end = Number.parseInt(range[2], 10);
|
||||
const spilloverEndVerse = range[3] ? Number.parseInt(range[3], 10) : null;
|
||||
if (start < 1 || end < 1 || start > end || start > maxVerse || end > maxVerse) {
|
||||
invalidCount += 1;
|
||||
return;
|
||||
}
|
||||
if (spilloverEndVerse !== null) {
|
||||
if (!nextChapter
|
||||
|| nextChapter.bookAbbrev !== activeChapter.bookAbbrev
|
||||
|| spilloverEndVerse < 1
|
||||
|| spilloverEndVerse > nextMaxVerse
|
||||
|| end !== maxVerse) {
|
||||
invalidCount += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
validRanges.push({ start, end, spilloverEndVerse });
|
||||
});
|
||||
|
||||
if (validRanges.length === 0) {
|
||||
setStatusMessage(`No valid ranges found. Use 1-${maxVerse} and format like 1-6, 7-12.`);
|
||||
window.setTimeout(() => setStatusMessage(''), 2600);
|
||||
return;
|
||||
}
|
||||
|
||||
const { addedCount, skippedCount } = addChunkRanges(validRanges);
|
||||
const notes = [];
|
||||
if (addedCount > 0) notes.push(`Added ${addedCount} chunk${addedCount > 1 ? 's' : ''}`);
|
||||
if (skippedCount > 0) notes.push(`skipped ${skippedCount} duplicate${skippedCount > 1 ? 's' : ''}`);
|
||||
if (invalidCount > 0) notes.push(`ignored ${invalidCount} invalid`);
|
||||
setStatusMessage(notes.join(' • '));
|
||||
window.setTimeout(() => setStatusMessage(''), 2800);
|
||||
|
||||
if (addedCount > 0) {
|
||||
setTypedChunkBulk('');
|
||||
}
|
||||
};
|
||||
|
||||
const addClickSpanChunk = () => {
|
||||
if (!activeChapter || rangeStart === null) {
|
||||
setStatusMessage('Click a start verse first, then set next chapter end.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
|
||||
if (!nextChapter) {
|
||||
setStatusMessage('Add the next chapter first to span into it.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (nextChapter.bookAbbrev !== activeChapter.bookAbbrev) {
|
||||
setStatusMessage('Click-based spanning only works into the next chapter of the same book.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2600);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMaxVerse = nextChapter.verses.at(-1)?.number ?? 0;
|
||||
const spilloverEndVerse = Number.parseInt(clickedSpanNextEnd, 10);
|
||||
if (!Number.isInteger(spilloverEndVerse) || spilloverEndVerse < 1 || spilloverEndVerse > nextMaxVerse) {
|
||||
setStatusMessage(`Next chapter end must be between 1 and ${nextMaxVerse}.`);
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = rangeStart;
|
||||
const end = activeChapter.verses.at(-1)?.number ?? start;
|
||||
const { addedCount } = addChunkRanges([{ start, end, spilloverEndVerse }]);
|
||||
if (addedCount === 0) {
|
||||
setStatusMessage('That cross-chapter chunk already exists.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
|
||||
setClickedSpanNextEnd('');
|
||||
setRangeStart(null);
|
||||
setRangeEnd(null);
|
||||
setStatusMessage('Cross-chapter chunk added.');
|
||||
window.setTimeout(() => setStatusMessage(''), 1800);
|
||||
};
|
||||
|
||||
const handleVerseClick = (verseNumber, event) => {
|
||||
if (event.shiftKey && rangeStart !== null) {
|
||||
const start = Math.min(rangeStart, verseNumber);
|
||||
const end = Math.max(rangeStart, verseNumber);
|
||||
addChunk(start, end);
|
||||
setRangeStart(null);
|
||||
setRangeEnd(null);
|
||||
return;
|
||||
}
|
||||
setRangeStart(verseNumber);
|
||||
setRangeEnd(verseNumber);
|
||||
};
|
||||
|
||||
const updateChunk = (chunkId, patch) => {
|
||||
updateProject((current) => ({
|
||||
...current,
|
||||
@@ -563,55 +126,12 @@ const App = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const moveChunk = (chunkId, direction) => {
|
||||
updateProject((current) => ({
|
||||
...current,
|
||||
chapters: current.chapters.map((ch) => {
|
||||
const index = ch.chunks.findIndex((c) => c.id === chunkId);
|
||||
if (index < 0) return ch;
|
||||
const nextIndex = index + direction;
|
||||
if (nextIndex < 0 || nextIndex >= ch.chunks.length) return ch;
|
||||
const updated = [...ch.chunks];
|
||||
const [removed] = updated.splice(index, 1);
|
||||
updated.splice(nextIndex, 0, removed);
|
||||
return { ...ch, chunks: updated };
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteChunk = (chunkId) => {
|
||||
updateProject((current) => {
|
||||
const newAllChunks = current.chapters
|
||||
.flatMap((ch) => ch.chunks)
|
||||
.filter((c) => c.id !== chunkId);
|
||||
const nextSelected =
|
||||
current.selectedChunkId === chunkId
|
||||
? newAllChunks[0]?.id ?? null
|
||||
: current.selectedChunkId;
|
||||
return {
|
||||
...current,
|
||||
selectedChunkId: nextSelected,
|
||||
chapters: current.chapters.map((ch) => ({
|
||||
...ch,
|
||||
chunks: ch.chunks.filter((c) => c.id !== chunkId),
|
||||
})),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Home page helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const openNewProject = () => {
|
||||
setProject(null);
|
||||
setActiveChapterIndex(0);
|
||||
setShowAddChapterForm(false);
|
||||
setRangeStart(null);
|
||||
setRangeEnd(null);
|
||||
setErrorMessage('');
|
||||
setStatusMessage('');
|
||||
setTitleEdited(false);
|
||||
setCurrentPage('setup');
|
||||
};
|
||||
|
||||
@@ -624,10 +144,6 @@ const App = () => {
|
||||
setProjectIndex(loadProjectIndex());
|
||||
setCurrentPage('home');
|
||||
setProject(null);
|
||||
setRangeStart(null);
|
||||
setRangeEnd(null);
|
||||
setErrorMessage('');
|
||||
setStatusMessage('');
|
||||
};
|
||||
|
||||
// Manual retry after a failed background sync (shown in the project header).
|
||||
@@ -668,34 +184,9 @@ const App = () => {
|
||||
...audio,
|
||||
// Reader
|
||||
...reader,
|
||||
// Setup
|
||||
// Setup (chunk-builder state lives in SetupPage via useChunkBuilder)
|
||||
project, setProject,
|
||||
setup,
|
||||
titleEdited, setTitleEdited,
|
||||
activeChapterIndex, setActiveChapterIndex,
|
||||
activeChapter,
|
||||
showAddChapterForm, setShowAddChapterForm,
|
||||
rangeStart, setRangeStart,
|
||||
rangeEnd, setRangeEnd,
|
||||
verseSearch, setVerseSearch,
|
||||
typedChunkStart, setTypedChunkStart,
|
||||
typedChunkEnd, setTypedChunkEnd,
|
||||
typedChunkNextEnd, setTypedChunkNextEnd,
|
||||
typedChunkBulk, setTypedChunkBulk,
|
||||
clickedSpanNextEnd, setClickedSpanNextEnd,
|
||||
loadingChapter,
|
||||
errorMessage,
|
||||
statusMessage,
|
||||
allChunks,
|
||||
handleSetupField,
|
||||
handleLoadChapter,
|
||||
beginStudying,
|
||||
handleVerseClick,
|
||||
addTypedChunk,
|
||||
addBulkTypedChunks,
|
||||
addClickSpanChunk,
|
||||
moveChunk,
|
||||
deleteChunk,
|
||||
updateProject,
|
||||
// Study (editor state lives in StudyPage and its hooks)
|
||||
updateChunk,
|
||||
|
||||
Reference in new issue
Block a user