Steps to reproduce
- Open any chapter on
origin/master v2.1.5 (be0106d47) — scroll mode (same in paged reader).
- Note the saved position, e.g. 35% at paragraph ~120.
- Tap the search (magnifier) in the reader appbar → type a query with many hits (e.g.
the, he said).
- Step through matches with next/prev (
chevron-down / chevron-up) — each step scrolls the WebView via window.readerSearch.next() / previous().
- Close search via X, hardware back, or tapping the magnifier again.
- Reopen the chapter / check Library progress / inspect
MMKV chapter_progress_{chapterId}.
Related repro for layout:
7. On a device where StatusBar.currentHeight is null (some Android builds / split-screen), open search and observe the searchbar overlapping the status bar.
Expected behavior
- Searching is a transient lookup — it must never overwrite the saved last-read position while the search overlay is open. The position should stay at the anchor where search was opened until the user explicitly chooses to stay.
- On close, the user should be offered a way back to that anchor (with a preference: auto-return after a short countdown, return immediately, or stay at the found match). When TTS is active, return should prioritize the active TTS highlight; in paged mode it should return to the anchored page; otherwise to the anchored scroll offset.
- The searchbar should be inset below the status bar on all devices and behave as an overlay (no layout shift), with animated enter/exit, a clear button, correct disabled states, and a truncation-aware counter.
Actual behavior
1) Progress is mutated while searching
src/screens/reader/components/WebViewReader.tsx on origin/master has no isSearchActive guard around the save handler. Every scroll caused by stepping through matches fires saveProgress(percent, paragraphIndex) via the onMessage → save path, so chapter_progress_{chapterId} and DB progress are overwritten to the current match location even though the user is only searching.
2) Position is lost on close
src/screens/reader/ReaderScreen.tsx on origin/master:
handleCloseSearch / handleToggleSearch only do setSearchVisible(false) + window.readerSearch.clear() — no anchor is captured on open (window.__searchAnchorY / __searchAnchorPage / __searchAnchorPIdx / __isSearching do not exist).
- Closing search leaves the viewport at the last match with no affordance to return. Because (1) already mutated progress, even reopening the chapter lands at the wrong place.
3) No user preference
src/hooks/persisted/useSettings.ts has no ChapterGeneralSettings.searchReturnBehavior — users who want to stay at the match and users who want to return cannot both be satisfied.
4) Searchbar layout/UX regressions
src/screens/reader/components/ReaderSearchbar.tsx reads StatusBar.currentHeight directly — null on several Android builds ⇒ bar renders under the status bar. It is a normal flex row in the hidden-header branch (not an absolute overlay), so it shifts layout and has no elevation.
- No enter/exit animation, no clear (×) button,
onSubmitEditing calls onNext() even with no results, no elevation, font/counter sizes not scaled via uiScale.
- Counter renders only
current/total — WebView caps highlights at MAX_RENDERED_MATCHES (1500) but renderedTotal / isTruncated (already in src/screens/reader/types/index.ts) are ignored, so truncation is silent.
- Missing accessibility labels (
closeSearch / clearSearch / previousMatch / nextMatch, Match X/Y announcement).
- Input is bound to the parent
searchResult.query (WebView round-trip) with a fragile debounce — typing feels laggy.
LNReader version
2.1.5 (origin/master be0106d47 — the v2.1.5 release containing 932638119 feat(reader): add in-chapter search)
Android version
Android 14 (reproducible on Android 13–15; not OS-specific — bug is in origin/master reader logic, status-bar overlap also seen on Android 12 where StatusBar.currentHeight returns null)
Device
Pixel 7 (also reproduced on generic emulator — any device; status-bar overlap is device/ROM-specific where StatusBar.currentHeight == null)
Other details
Impact
- Data loss: Search moves the permanent last-read position — users lose their place without realizing it.
- Navigation: Deep search → close → manual scroll back with no anchor.
- Layout: Searchbar overlaps status bar on ROMs where
StatusBar.currentHeight is null.
Proposed solution (already proven working on local dev)
The fix is implemented and verified on local dev (3 commits ahead of origin/master: 74404ed43 feat(reader): enhance in-chapter search UX, fbf73b4e3 fix(reader): pass status bar height to searchbar, dd53eeb3e fix(reader): handle back press for return banner; pnpm run type-check + pnpm run test pass, manual QA in scroll/paged/TTS).
A) Make search non-destructive
- On open, capture
window.__searchAnchorY / __searchAnchorPage / __searchAnchorPIdx and __isSearching = true before any search scroll; unhide header if hidden.
- Add
isSearchActive (searchVisible || showReturnBanner) prop to WebViewReader → isSearchActiveRef guard that early-returns from the save message handler (save-ignore-while-searching). On close paths: clear anchors/__isSearching; when staying, call window.reader.saveProgress() to commit the current position, otherwise just jump — the anchor is last-read.
B) Configurable return-on-close
- New
ChapterGeneralSettings.searchReturnBehavior: 'countdown' | 'immediate' | 'stay' (default countdown), exposed in Settings → Reader → Navigation via SearchReturnBehaviorModal (3 radio options, persisted with setChapterGeneralSettings).
countdown: close → bottom banner “Returning to last read position in 5s” with Return now / Stay here, auto-return after 5s (setInterval, cleared on unmount / chapter.id change / dismiss / explicit close). immediate: instant executeReturnToAnchor(). stay: no jump, and commits current position. Toggle-close (magnifier while open) dismisses without banner — intentional “leave me here”.
- Return priority: active TTS highlight (
window.tts.scrollToElement) → paged reader (pageReader.movePage) → scroll (window.scrollTo({ behavior: 'smooth' })). Hardware back priority: searchVisible → return banner → drawer.
C) Searchbar polish
- Receive
statusBarHeight from NovelContext (ChapterContent → ReaderSearchbar) with fallback to StatusBar.currentHeight, render as position: absolute; top:0; zIndex:2; elevation:4 overlay with paddingTop: statusBarHeight + padding.xs. Add react-native-reanimated enter/exit. Use controlled local inputValue with 150ms debounce, clear (×) button, Keyboard.dismiss() on close, onSubmitEditing only when hasResults, correct disabled stepper states, scaleDimension(..., uiScale) sizing, truncation-aware counter current/renderedTotal+ + (N total) when isTruncated, and accessibility labels.
No DB migration needed; safe default. Files: src/hooks/persisted/useSettings.ts, src/screens/reader/ReaderScreen.tsx, src/screens/reader/components/ReaderSearchbar.tsx, src/screens/reader/components/WebViewReader.tsx, src/screens/settings/SettingsReaderScreen/Modals/SearchReturnBehaviorModal.tsx + strings.
Repro after fix (expected)
- Open chapter → search → step through matches → progress unchanged until close.
- Close with default
countdown → banner → auto-return (or Return now / Stay here / hardware back all behave); immediate → instant return; stay → no return.
- Works in scroll, paged, and TTS-active chapters; survives chapter change/rotation; correctly inset when
StatusBar.currentHeight == null.
Acknowledgements
Steps to reproduce
origin/masterv2.1.5 (be0106d47) — scroll mode (same in paged reader).the,he said).chevron-down/chevron-up) — each step scrolls the WebView viawindow.readerSearch.next()/previous().MMKV chapter_progress_{chapterId}.Related repro for layout:
7. On a device where
StatusBar.currentHeightisnull(some Android builds / split-screen), open search and observe the searchbar overlapping the status bar.Expected behavior
Actual behavior
1) Progress is mutated while searching
src/screens/reader/components/WebViewReader.tsxonorigin/masterhas noisSearchActiveguard around thesavehandler. Every scroll caused by stepping through matches firessaveProgress(percent, paragraphIndex)via theonMessage→savepath, sochapter_progress_{chapterId}and DB progress are overwritten to the current match location even though the user is only searching.2) Position is lost on close
src/screens/reader/ReaderScreen.tsxonorigin/master:handleCloseSearch/handleToggleSearchonly dosetSearchVisible(false)+window.readerSearch.clear()— no anchor is captured on open (window.__searchAnchorY/__searchAnchorPage/__searchAnchorPIdx/__isSearchingdo not exist).3) No user preference
src/hooks/persisted/useSettings.tshas noChapterGeneralSettings.searchReturnBehavior— users who want to stay at the match and users who want to return cannot both be satisfied.4) Searchbar layout/UX regressions
src/screens/reader/components/ReaderSearchbar.tsxreadsStatusBar.currentHeightdirectly —nullon several Android builds ⇒ bar renders under the status bar. It is a normal flex row in the hidden-header branch (not an absolute overlay), so it shifts layout and has no elevation.onSubmitEditingcallsonNext()even with no results, no elevation, font/counter sizes not scaled viauiScale.current/total— WebView caps highlights atMAX_RENDERED_MATCHES(1500) butrenderedTotal/isTruncated(already insrc/screens/reader/types/index.ts) are ignored, so truncation is silent.closeSearch/clearSearch/previousMatch/nextMatch,Match X/Yannouncement).searchResult.query(WebView round-trip) with a fragile debounce — typing feels laggy.LNReader version
2.1.5 (
origin/masterbe0106d47— the v2.1.5 release containing932638119 feat(reader): add in-chapter search)Android version
Android 14 (reproducible on Android 13–15; not OS-specific — bug is in
origin/masterreader logic, status-bar overlap also seen on Android 12 whereStatusBar.currentHeightreturns null)Device
Pixel 7 (also reproduced on generic emulator — any device; status-bar overlap is device/ROM-specific where
StatusBar.currentHeight == null)Other details
Impact
StatusBar.currentHeightis null.Proposed solution (already proven working on local
dev)The fix is implemented and verified on local
dev(3 commits ahead oforigin/master:74404ed43 feat(reader): enhance in-chapter search UX,fbf73b4e3 fix(reader): pass status bar height to searchbar,dd53eeb3e fix(reader): handle back press for return banner;pnpm run type-check+pnpm run testpass, manual QA in scroll/paged/TTS).A) Make search non-destructive
window.__searchAnchorY/__searchAnchorPage/__searchAnchorPIdxand__isSearching = truebefore any search scroll; unhide header if hidden.isSearchActive(searchVisible || showReturnBanner) prop toWebViewReader→isSearchActiveRefguard that early-returns from thesavemessage handler (save-ignore-while-searching). On close paths: clear anchors/__isSearching; when staying, callwindow.reader.saveProgress()to commit the current position, otherwise just jump — the anchor is last-read.B) Configurable return-on-close
ChapterGeneralSettings.searchReturnBehavior: 'countdown' | 'immediate' | 'stay'(defaultcountdown), exposed in Settings → Reader → Navigation viaSearchReturnBehaviorModal(3 radio options, persisted withsetChapterGeneralSettings).countdown: close → bottom banner “Returning to last read position in 5s” with Return now / Stay here, auto-return after 5s (setInterval, cleared on unmount /chapter.idchange / dismiss / explicit close).immediate: instantexecuteReturnToAnchor().stay: no jump, and commits current position. Toggle-close (magnifier while open) dismisses without banner — intentional “leave me here”.window.tts.scrollToElement) → paged reader (pageReader.movePage) → scroll (window.scrollTo({ behavior: 'smooth' })). Hardware back priority:searchVisible→ return banner → drawer.C) Searchbar polish
statusBarHeightfromNovelContext(ChapterContent→ReaderSearchbar) with fallback toStatusBar.currentHeight, render asposition: absolute; top:0; zIndex:2; elevation:4overlay withpaddingTop: statusBarHeight + padding.xs. Addreact-native-reanimatedenter/exit. Use controlled localinputValuewith 150ms debounce, clear (×) button,Keyboard.dismiss()on close,onSubmitEditingonly whenhasResults, correct disabled stepper states,scaleDimension(..., uiScale)sizing, truncation-aware countercurrent/renderedTotal++(N total)whenisTruncated, and accessibility labels.No DB migration needed; safe default. Files:
src/hooks/persisted/useSettings.ts,src/screens/reader/ReaderScreen.tsx,src/screens/reader/components/ReaderSearchbar.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/settings/SettingsReaderScreen/Modals/SearchReturnBehaviorModal.tsx+ strings.Repro after fix (expected)
countdown→ banner → auto-return (or Return now / Stay here / hardware back all behave);immediate→ instant return;stay→ no return.StatusBar.currentHeight == null.Acknowledgements