Adjusting the top, bottom, left, or right page margin had no visible
effect until an unrelated setting (e.g. Show Header) was toggled.
The BooksGrid perf refactor (#4562) memoized the derived view/content
insets on the ViewSettings object identity. saveViewSettings mutates
ViewSettings in place (same reference), so the memo never recomputed on
a margin edit and the new margin never reached the paginator. Left and
right margins were always stale; top and bottom only refreshed when the
header/footer visibility (an effect dependency) changed, which is why
toggling the header appeared to apply a pending change.
Extract the inset derivation into useContentInsets and memoize by the
resolved numeric values instead of the object reference: identical
numbers across a page turn keep a stable reference (no re-render storm),
while a changed margin yields a new one that propagates to the renderer.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersedes #3156. Adds a reading-statistics system whose canonical data model
is KOReader's own (book + page_stat_data), so stats round-trip losslessly
between Readest and KOReader.
- Storage: a cross-platform Turso statistics.db in KOReader's exact schema
(web/Workers, desktop, iOS, Android) — replacing #3156's Node-only
better-sqlite3 + statistics.json.
- Tracking: per-page reading events (time-on-page, idle-capped) flushed on
page-change/idle/hide/close — the KOReader model — not session aggregates.
- Sync: legacy /api/sync extended with a stats type backed by self-contained
Supabase tables (stat_books, stat_pages); union/longer-duration-wins merge
keyed on book_hash. apps/readest.koplugin syncs through the same endpoint.
- Scale & robustness: per-tab singleton connection (avoids OPFS lock
conflicts) + explicit WAL checkpoint; transactional bulk apply; chunked
resumable push; client-driven paged pull with trailing-ms completion;
paginated/scoped server merge.
Verified: 5668 unit tests, 155 koplugin busted tests, biome+tsgo + luacheck
all green; web OPFS DB verified live.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(reader): coalesce relocate events and memoize BookCell to stop per-swipe storm
FoliateViewer
-------------
foliate fires `relocate` multiple times during a swipe burst (snap
steps + intermediate stabilize). Each one ended up in setProgress,
which writes to readerProgressStore + bookDataStore. Coalesce them to
a single commit per animation frame so only the final viewport state
is persisted.
Earlier this used requestIdleCallback, but profiling on Android showed
"Fire Idle Callback" ballooning to 2.0+ s of total time per ~28 s
session: rIC backed up under sustained pressure and dumped the whole
queue into the post-swipe pause, producing exactly the "feels sluggish
right after I let go" jank we were trying to fix. rAF runs once per
frame, gets scheduled by the browser's normal vsync loop, and doesn't
accumulate when the page is busy.
BooksGrid -> BookCell
---------------------
Previously BooksGrid subscribed to the entire progresses map and
rendered every book inline. The map changes on every page turn, so the
whole bookKeys.map(...) body re-ran for every swipe. On top of that
inset-related objects (gridInsets, contentInsets) were rebuilt every
render and threaded as fresh references into 7+ children, so even
unchanged children couldn't bail out. That accounted for ~27% of
main-thread time in the Bottom-Up profile ("Animation Frame Fired"
2.6s / 27%).
Extract BookCell as its own React.memo'd component:
- Each cell subscribes only to its own book's progress via
useBookProgress(bookKey). A page turn re-renders one BookCell, not
the grid.
- viewInsets / contentInsets are memoized off their numeric inputs so
children get stable prop references across renders.
- BookCell uses per-field selectors internally for the same reason
spelled out in store/readerProgressStore.ts header.
- Dropdown handlers are wrapped in useCallback so HeaderBar's props
object stays stable.
* fix(reader): subscribe BookCell to its own viewState so settings/ribbon toggles apply live
BookCell subscribed reactively only to useBookProgress and read
viewState/viewSettings imperatively. Settings that save with
applyStyles=false (Show Header/Footer, Double Border, Border Color) and
the bookmark ribbon toggle write no progress, so the cell didn't
re-render and the chrome it gates (SectionInfo, ProgressBar, DoubleBorder,
Ribbon) only updated on the next page turn.
Subscribe to the per-book viewStates[key] slice. This is safe now that
progress lives in its own store — viewStates[key] only bumps on
low-frequency events (settings toggles, ribbon, init, sync), never on
the per-swipe relocate path — so it does not reintroduce the commit
storm the progress-store split removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reader): turn automatically when highlighting across pages (closes#1354)
* refact: Refactor time retrieval to use Date.now()
* fix(reader): rework auto page-turn as a corner-dwell gesture
Rework the initial #1354 implementation into a deliberate corner-dwell
gesture that works across platforms, and fix popup positioning for
cross-page selections.
Trigger:
- While a text selection is active, hold any engagement signal — the
pointer (web/desktop/iOS), the Android native touchmove, or the
selection caret — inside a screen corner for 500ms to turn one page:
bottom-right goes to the next page, top-left to the previous.
- One turn per engagement: a signal must leave the corner and return to
turn another page, so the user controls it one page at a time.
- The corner is a quarter-ellipse of radius 15% of each axis, measured
against the reading frame (the <foliate-view> rect) inset by the page
content margins, so the zone lands on the text — not the margin/footer
or a sidebar — and the pointer can actually reach it.
Per-platform signals:
- web/desktop/iOS: the iframe pointermove, mapped to window coordinates
via the iframe element's on-screen rect.
- Android: the selection caret (the only signal during a native handle
drag, where the handles live in a separate window so their touches
never reach the Activity) plus a throttled (~10/s) native touchmove
added in MainActivity.dispatchTouchEvent for content drags.
Android scroll-pin (#873): an active selection pins the container scroll,
which reverted the turn; suspend the pin during the turn and re-anchor it
to the page we land on.
Popup positioning: getPosition decided which selection end was on-screen
using window bounds, so a cross-page selection's off-screen start (which
maps behind the sidebar but inside the window) read "in view" and pinned
the popup off the visible page. Test visibility against the reading frame
instead, and for a multi-page selection anchor to the last on-screen line.
Also: logical view.prev()/next() (RTL-correct); skip in scrolled mode;
pass contentInsets down to the annotator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the redundant "Apply also in Scrolled Mode" options for bars and
margins so scrolled mode renders the header/footer consistently with
paginated mode: transparent, fixed in position, and not obscuring content.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* added current time to desktop bar
* added time prototype to footer, needs code cleanup and settings toggle
* fixed settings toggle, added translations and code cleanup
* added battery support and moved Statusbar to own Component
* #3306 added 24 hour clock support
* refactored code styling and getting rid of any type in battery hook
* Add battery info for Tauri Apps
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* fix(settings): ensure latest config is used when saving view settings
* fix: reader ruler appearing before other book reading contents
* feat(reader): dispatch reader-closing event during book close
* feat(reader) add auto reposition to top when changing pages and position persistence on book close
* fix(reader): add rtl prop to ReadingRuler component
* fix(reader): handle vertical ltr writing mode in ruler auto positioning
* chore: revert redundant changes to settings.ts
* refactor: remove redundant reader-closing
* refactor: use store progress for ruler positioning with throttled saving
Add a floating navigation component that appears when search results exist.
The component includes:
- Top bar: displays search term and current section with TOC and close buttons
- Bottom bar: search results, previous, and next navigation buttons
- Page-based navigation using isCfiInLocation to skip between pages with results