* feat(reader): swipe down to re-fetch a chapter, never cache empty responses
- pull down at the top of a chapter to re-fetch it, recovering from
transient empty responses without leaving the reader
- empty responses are evicted from the chapter cache so a refresh
re-fetches instead of replaying the empty message
- add a refresh button to the reader's bottom bar and a 'Refresh
chapter' link on the empty-chapter message
- register the typescript-eslint plugin and ignore assets/ in the
eslint flat config so lint-staged can lint core.js
* feat(reader): move refresh option to appbar menu
[skip ci]
* perf(reader): Cut Chapter Open Latency and Reading-Time Re-Renders
Opening a chapter blocked the first paint on work that is not needed to
show text, and mounted several heavy subtrees while that work was running.
Load path (useChapter):
- Render the chapter as soon as its text is ready. Resolving the next and
previous chapter (two full-scan queries, plus a plugin page fetch over
the network at a page boundary) now runs after the chapter is on screen
and is pushed into the WebView instead of being baked into its HTML.
- Read downloaded chapters with a single native call instead of
exists() + readFile(), and start that read in parallel with the
database lookup instead of after it.
- Cache render-ready (sanitized) HTML, so prefetched chapters skip
sanitize-html entirely, and prefetch when the JS thread is idle rather
than in the middle of the current load.
- Drop failed loads from the cache so a retry actually retries.
- Guard every load with a generation id, keep getChapter and friends
referentially stable, and defer history/last-read writes off the
critical path.
Mounting:
- The chapter drawer (a list of every chapter) and the settings sheet
(a tab view that enumerates TTS engines and voices) are mounted on
first use instead of at reader open; the sheet's tabs are lazy.
- The loading skeleton shimmers its first lines only instead of
animating a full screen of gradient views.
While reading:
- Progress and mark-as-read writes keep the chapter array identity when
nothing changed, and a chapter is marked read (and pushed to the
tracker) once instead of on every progress report.
- Drawer rows are a memoized component with stable props, the drawer
stylesheet is memoized, and the WebView document is built once per
chapter instead of on every reader re-render.
Also fixes the drawer's loading branch, which compared a ref object to
undefined and was therefore unreachable, and guards page-reader
transitions against a not-yet-resolved neighbour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ac6DNTqrKqsQjbdMSmU3nm
* perf(reader): Stop Double-Parsing the Chapter and Cut Per-Frame Work
Follow-up to the previous pass, mostly inside the reader document itself.
Chapter open:
- The chapter body was written back into the DOM on every open. The
document already arrives with the chapter parsed, so unless bionic
reading or paragraph-spacing removal is on, that re-parsed and
re-laid-out the whole chapter (and restarted image loads) for nothing.
It now only writes when the transformed markup actually differs.
- The reading position was restored from `window.load` via
`document.fonts.ready`, both of which wait for every image in the
chapter - measured at ~1.5s with a single slow image, during which the
reader sat at the top of the chapter. It is now restored once the
chapter is parsed and styled (~100ms in the same scenario) with an
instant scroll, and corrected afterwards if fonts or images changed the
height - but only while the reader is still where it was put, so it
cannot yank the page from under someone already reading.
While reading:
- The scrollbar and the footer percentage each had their own unthrottled
scroll listener recomputing the same ratio; they now share one
listener coalesced to a frame. The footer clock no longer ticks state
when the footer is hidden.
- Reading time is persisted without rebuilding the in-memory chapter
list. It was rebuilt every few seconds for a field no screen renders -
the statistics screen aggregates it from the database - and on Fabric
react-navigation deliberately does not freeze the screen below the
focused one, so it re-rendered the novel screen's chapter list too.
- `hidden` moved into its own context. It flips on every tap on the page,
and a context value change re-renders all consumers, so the WebView,
drawer, appbar and searchbar were all re-rendering for a flag only the
reader screen reads.
- `openPage` no longer re-queries the chapter list when the requested
page is already open, which the drawer does on every mount.
Smaller: the battery level is read asynchronously instead of through two
synchronous native calls per chapter, the sanitizer options object is
built once instead of per chapter, the general settings tab draws its
dozen fixed rows directly instead of virtualising them, and the reader
settings hook stops allocating a new settings object per render.
Verified the document changes by loading the real reader assets in
Chromium: position restored before load, no body rewrite when no
transform applies, both scroll indicators driven by the shared listener,
progress still reported, transforms still applied and reverted, and the
post-load correction only firing on an untouched reader.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ac6DNTqrKqsQjbdMSmU3nm
* perf(reader): Optimize Search Functionality with Caching and Batch Processing
---------
Co-authored-by: Claude <noreply@anthropic.com>