commit 02e190246e2510024bec6c929acfd2631ad64271 truex <truex@equwal.com> 2026-09-26 23:23:50 -0700 Do not let late meta data undo a change of the user The meta data of a book can come many seconds after the open: getMeta waits behind the database upgrade. Two changes of the user were lost: - The user opens B and ticks Force OCR before the meta data of B comes. applyMeta then set Force OCR back and started the reading again without it, while the store said on. - The user opens B. Page 1 shows, and a save of page 1 waits. The user opens C before the meta data of B comes: the save of page 1 went over the saved page of B, and the meta data of B never applied. state.changed now notes a page turn (by the user or by the follow) and a change of Force OCR after the open. lateMeta (book-open.ts) gives the fields of the meta data that apply: each field that did not change. The first page of an open is the saved page or a placeholder, and the page of the meta data is the saved page, so showPage does not save them. Only a page turn saves the page.
README.md | 3 +++ src/book-open.ts | 27 ++++++++++++++++++++++++++- src/main.ts | 41 +++++++++++++++++++++++++++++------------ test/book-open.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md index 67905fc..ae5297d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ second after it changes. The first start after an upgrade from a build with the old dictionary can take long: the upgrade deletes about 86 MB of dictionary data. The strip then says "Updating the page cache...", and the start card shows. +A book that you open in that time shows at once, and its meta data +applies when it comes. A page turn or a change of "Force OCR" that you +make before then stays. ### Android: Back, the screen and the suite diff --git a/src/book-open.ts b/src/book-open.ts index a16297e..d8b8c14 100644 --- a/src/book-open.ts +++ b/src/book-open.ts @@ -1,5 +1,8 @@ // The open of a book: the open attempts, the owner of subtitles that load -// during an open, and the restore of the last book. Pure: no DOM. +// during an open, the restore of the last book, the error of a failed +// attempt, and the meta data that comes after the open. Pure: no DOM. + +import type { BookMeta } from './token-cache'; /** * The open attempts of books. Each attempt gets the next number: 1, 2, 3 ... @@ -82,3 +85,25 @@ export function userOpened(o: Opens, since: number): boolean { export function failureShows(o: Opens, attempt: number, since?: number): boolean { return attempt > o.shown && (since === undefined || !userOpened(o, since)); } + +/** What changed after the open of a book, before its meta data came. */ +export interface Changes { + /** A page turn: by the user, or by the follow of the audio. */ + page: boolean; + /** A change of "Force OCR" by the user. */ + forceOcr: boolean; +} + +/** + * The fields of the meta data of a book that apply when the meta data comes + * after the open: each field that did not change after the open. A field + * that is not in the result does not apply. Without a saved setting, "Force + * OCR" is off. The upgrade of the database can hold the meta data for many + * seconds, and before, it set "Force OCR" back after the user ticked it. + */ +export function lateMeta(meta: BookMeta, changed: Changes): { page?: number; forceOcr?: boolean } { + const late: { page?: number; forceOcr?: boolean } = {}; + if (!changed.page && meta.page !== undefined) late.page = meta.page; + if (!changed.forceOcr) late.forceOcr = meta.forceOcr ?? false; + return late; +} diff --git a/src/main.ts b/src/main.ts index 8817e2d..3fb0ff4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,8 +14,10 @@ import { createOpens, endOpen, failureShows, + lateMeta, subtitlesOwner, userOpened, + type Changes, } from './book-open'; import { keepMakeStatus, @@ -186,6 +188,8 @@ const state = { dbSlow: false, /** "Force OCR" of the open book: the reading skips the text layer. */ forceOcr: false, + /** What changed after the open of the book. Its meta data does not undo it (lateMeta). */ + changed: { page: false, forceOcr: false } as Changes, /** A SubRead job that waits until all pages of its book are read. */ makeWait: null as { audio: { uri: string; name: string }; book: string } | null, /** The book of the SubRead job that runs, or null. */ @@ -283,6 +287,8 @@ ui.lang.addEventListener('change', () => { ui.forceOcr.addEventListener('change', () => { const book = state.book; if (book === null) return; + // Meta data of the book that comes later does not undo this (lateMeta). + state.changed.forceOcr = true; setForceOcr(ui.forceOcr.checked); void putMeta(book, { forceOcr: state.forceOcr }); void readBook(); @@ -384,6 +390,7 @@ async function openBook(file: File, restore?: Restore): Promise<boolean> { state.activeCue = -1; state.markedCue = -1; state.held = false; + state.changed = { page: false, forceOcr: false }; // The subtitles of another book go at once. Subtitles of no book, loaded // while no book was open or while this book loaded, are for this book. if (state.subtitles && state.subtitlesBook !== null && state.subtitlesBook !== key) { @@ -407,7 +414,9 @@ async function openBook(file: File, restore?: Restore): Promise<boolean> { if (state.clock) showPlayerStatus(state.clock); say(pdfLoadedText(pdf.numPages)); // showPage sets the current page at once, so the reading starts there. - void showPage(validPage(meta?.page, pdf.numPages) ?? 0); + // This page is the saved page, or page 1 until the meta data comes: no + // save, so page 1 does not go over the saved page of the book. + void showPage(validPage(meta?.page, pdf.numPages) ?? 0, false); // readBook stops the reading of the old book before its document closes. void readBook(); void old?.destroy(); @@ -431,20 +440,19 @@ function validPage(page: number | undefined, pages: number): number | undefined /** * Applies the meta data of the book that open attempt `attempt` opened. * `subtitlesSeq` is the count of subtitle loads at the open: subtitles - * that the user loaded after the open stay. The page applies only while - * the first page shows, so a page turn after the open stays. + * that the user loaded after the open stay. A page turn and a change of + * "Force OCR" after the open stay too (lateMeta). */ function applyMeta(meta: BookMeta, attempt: number, subtitlesSeq: number): void { const key = state.book; const pdf = state.pdf; if (attempt !== opens.shown || key === null || !pdf) return; - const page = validPage(meta.page, pdf.numPages); - if (page !== undefined && page !== state.currentPage && state.currentPage === 0) { - void showPage(page); - } - const forceOcr = meta.forceOcr ?? false; - if (forceOcr !== state.forceOcr) { - setForceOcr(forceOcr); + const late = lateMeta(meta, state.changed); + const page = validPage(late.page, pdf.numPages); + // The saved page needs no new save. + if (page !== undefined && page !== state.currentPage) void showPage(page, false); + if (late.forceOcr !== undefined && late.forceOcr !== state.forceOcr) { + setForceOcr(late.forceOcr); void readBook(); } if (state.subtitlesSeq === subtitlesSeq) { @@ -1287,12 +1295,21 @@ function marks(index: number): Marks | null { }; } -async function showPage(index: number): Promise<void> { +/** + * Shows page `index`. `save` is true for a page turn by the user or by the + * follow: the page goes to the meta data of the book soon, and the meta + * data that comes later does not turn the page back (lateMeta). The page + * of the open and the saved page need no save. + */ +async function showPage(index: number, save = true): Promise<void> { const pdf = state.pdf; if (!pdf || index < 0 || index >= pdf.numPages) return; state.currentPage = index; nav.update(); - savePageSoon(); + if (save) { + state.changed.page = true; + savePageSoon(); + } try { await view.show(pdf, index); } catch (err) { diff --git a/test/book-open.test.ts b/test/book-open.test.ts index 83e161b..7b41e38 100644 --- a/test/book-open.test.ts +++ b/test/book-open.test.ts @@ -5,12 +5,52 @@ import { createOpens, endOpen, failureShows, + lateMeta, opening, subtitlesOwner, userOpened, type Opens, } from '../src/book-open'; +describe('lateMeta', () => { + const none = { page: false, forceOcr: false }; + + it('does not undo "Force OCR" that the user set before the meta data came', () => { + // The finding: the user opens B and ticks Force OCR before the meta data + // of B comes. The meta data set Force OCR back, and the reading started + // again without it, while the store said on. + expect(lateMeta({}, { page: false, forceOcr: true })).toEqual({}); + expect(lateMeta({ forceOcr: false }, { page: false, forceOcr: true })).toEqual({}); + }); + + it('does not go to the saved page after a page turn', () => { + expect(lateMeta({ page: 57 }, { page: true, forceOcr: false })).toEqual({ forceOcr: false }); + }); + + it('applies each field that did not change', () => { + expect(lateMeta({ page: 57, forceOcr: true }, none)).toEqual({ page: 57, forceOcr: true }); + // Without a saved setting, Force OCR is off. Without a saved page, the page stays. + expect(lateMeta({}, none)).toEqual({ forceOcr: false }); + }); + + it('gives exactly the fields that did not change, with their saved values', () => { + const meta = fc.record( + { page: fc.nat({ max: 500 }), forceOcr: fc.boolean() }, + { requiredKeys: [] }, + ); + const changes = fc.record({ page: fc.boolean(), forceOcr: fc.boolean() }); + fc.assert( + fc.property(meta, changes, (m, changed) => { + const late = lateMeta(m, changed); + expect('page' in late).toBe(!changed.page && m.page !== undefined); + if ('page' in late) expect(late.page).toBe(m.page); + expect('forceOcr' in late).toBe(!changed.forceOcr); + if ('forceOcr' in late) expect(late.forceOcr).toBe(m.forceOcr ?? false); + }), + ); + }); +}); + describe('failureShows', () => { it('does not say the error of an older pick over the book that shows', () => { // The finding: the user picks a large file A that is no PDF, then B. B