Skip to content

Foldable two-page reading, EPUB typesetting, and read-aloud position fixes - #1

Merged
chortya merged 13 commits into
mainfrom
feature/fold-dual-page
Oct 11, 2026
Merged

chortya merged 13 commits into
mainfrom
feature/fold-dual-page

Conversation

@chortya

@chortya chortya commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Contribution pause

This pull request is within the fork (chortya/lagrange-reader), not upstream. Upstream general PRs are paused; upstream issue van-geaux#286 asks the maintainer whether they want any of this work before anything is proposed there.

Maintainer-requested exception

  • A maintainer requested this pull request or confirmed the exception. (Not yet — fork-only PR; see the upstream issue.)
  • The change is scoped to the requested work (foldable two-page reading, EPUB typesetting, read-aloud position and language).
  • The change is compatible with the multi-server/provider-module refactor. (Rebased onto upstream 2.0.0; reader-side only, no provider-module changes except one store clear on logout. Not reviewed by upstream.)
  • Tests and manual-validation status are documented.
  • No credentials, cookies, API keys, private server URLs, or generated local data are included.

Summary

Rebased onto upstream main 2.0.0 (9a55b6b). Plan and status: docs/fold-dual-page-plan.md.

Foldable two-page reading (Galaxy Z Fold8 inner display, landscape)

  • Spread policy from window geometry (ReaderSpreadPolicy, ReaderWindowTracker, androidx.window 1.5.1): two pages on a large horizontal paginated window, one page when folded, in portrait, in split-screen, in continuous layout, or in tabletop posture. Per-library Two-page spread when unfolded toggle in library settings and the in-book options.
  • EPUB: Readium two columns / fixed-layout Spread.ALWAYS, with RsProperties to bypass ReadiumCSS's 60em media query. Each page gets symmetric margins that follow the margin setting live.
  • Comics: new Compose spread pager (ComicSpreadReader, ComicSpreadPairing): cover alone, shift pairs, wide pages alone, RTL, zoom, export, spread navigation and resume. Pages meet at a flat fold with no gap.
  • PDF unchanged (consistent with upstream refactor: keep native PDFium navigation van-geaux/lagrange-reader#269).

EPUB reading

  • True OLED white-on-black theme (EPUB only; comics stay full colour).
  • Text alignment (Left/Justified) and Hyphenation (Auto/On/Off; Auto hyphenates justified text only). Default unchanged.
  • Per-book Book language (in-book options → This book): drives read-aloud voices and hyphenation; Auto = book metadata, else device language. Previously voices were locked to that language with no override.

Read-aloud position fixes (bugs also present upstream)

  • Rebuilding the reader from saved state (process death) preferred the stale saved-state position over the newer position saved by background read-aloud.
  • Returning to the foreground replayed the old visual position over the newer read-aloud position.
  • Rebuilding the reader followed the saved read-aloud position even with no read-aloud session running, jumping back to where listening last stopped.
  • Pausing read-aloud now sends the latest position to the server past the progress throttle.

Line endings: the last commit restores upstream's original CRLF/LF line endings in touched files, so the combined diff contains only real changes.

Verification

  • ./gradlew :app:testDebugUnitTest — 959/959 pass (with JAVA_TOOL_OPTIONS="-Duser.language=en -Duser.country=US", see docs/testing.md).
  • ./gradlew :app:lintDebug — 0 errors. :app:compileDebugAndroidTestKotlin and :app:assembleDebug succeed.
  • No trailing whitespace added (git diff --check flags only the CR of CRLF lines, the same as upstream's own 2.0.0 changes to those files).
  • Connected tests on a Pixel Fold API 36 emulator (2208×1840) after the rebase: EpubPositionRestoreInstrumentedTest, EpubBookLanguageInstrumentedTest, FoldSpreadOledActivityInstrumentedTest, ComicSpreadReaderInstrumentedTest, ReadiumEpubOpenInstrumentedTest, EpubReaderProgressFooterInstrumentedTest, EpubReaderOptionsOverlayInstrumentedTest — 53/60 pass. The 7 failures also fail on unmodified upstream on the same emulator (selectsARealReadiumPositionForNonzeroServerPercentage, generatedEpubImageColorOverrideIsUniversalAndIdempotent, and 5 in EpubReaderOptionsOverlayInstrumentedTest).
  • The three read-aloud position tests were written first and failed before their fixes. Read-aloud voice tests use the emulator's system speech engine.
  • Manual: test builds (fold-dual-page-test-* pre-releases) checked by the owner on a physical Galaxy Z Fold8: spreads, margins, typesetting, and read-aloud position. Not yet confirmed on device: half-open/tabletop posture, split-screen, fold/unfold during read-aloud, RTL manga, orientation lock, Fold8 Ultra.

🤖 Generated with Claude Code

Andriy Chortkivskyy and others added 13 commits October 11, 2026 10:20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a geometry-based spread policy (sw>=600dp, horizontal, paginated,
not tabletop) backed by Jetpack WindowManager posture, per-library
spread preferences with options UI, and Readium two-column/fixed-layout
spread configuration that bypasses ReadiumCSS's 60em media query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OLED black now renders pure #FFFFFF text and links on #000000 across the
page, reader chrome, options sheet, overlays, dialogs, window background
and status-bar strip. Selection inverts to white; ReadiumCSS night-mode
blue links and pale-blue selection are overridden in-page. Images keep
their original colours. Other reader themes and comics are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a Compose spread pager for paginated CBZ/CBR when the spread policy
applies: cover-alone and shift pairing, wide pages shown alone with live
re-pairing, RTL page order, hinge-aware gutter, bounded pinch zoom,
tap-zone/volume/page-list navigation by spread, per-page long-press
export, first-page resume and last-visible-page progress. Includes unit,
Compose UI and device-level spread/OLED instrumented tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Existing comic navigator tests now pin twoPageSpread=OFF so they test
the Readium image navigator on any window size; add spread-mode center
tap and pinch-zoom paging-lock tests. Use a gesture-local zoom scale so
fast pinches do not compound a stale scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each spread page now gets the same margin on its outer and inner side,
sized like the single-page margin for the same setting, and changes to
margins apply live. The container drops its horizontal padding in a
spread; Readium's per-column gutter is a 1px unit scaled by pageMargins.

Tests: JVM margin mapping; on-device measurement of all four page
margins, line pitch on both pages under line-spacing and font-size
changes, and the rotate-to-portrait fallback for EPUB and comics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds per-library Text alignment (Left/Justified) and Hyphenation
(Auto/On/Off; Auto hyphenates justified text only) to the reader
settings and the in-book options sheet, mapped to Readium textAlign and
hyphens and applied live. Books that declare no language get the device
language so the browser can hyphenate; RTL and CJK device languages are
skipped because Readium derives layout from the language.

Tests: JVM storage/fallback rules; on-device measurement of hyphenated
words and flush lines on both spread pages, the no-language fallback,
and the options-sheet controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Background read-aloud saves each utterance position after the reader
stopped. Two paths then put the reader back at the older visual page
and saved it over the newer position (and reported it to the server):

- Rebuilding the reader from saved instance state (process death)
  preferred the saved-state locator over the newer stored position.
  The stores, written on every move, now win; saved state is only a
  fallback (preview).
- Returning to the foreground replayed the navigator's last position
  through the locator flow and the pagination listener. Only real moves
  are recorded now; re-reports of the same place are written only while
  the store still holds this reader's own position.

Also send the current read-aloud position to the server when playback
is paused, past the 15 s / 0.2% progress throttle.

Tests: instrumented regression tests for both paths (red before the
fix) and for configuration recreation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Readium picks one read-aloud language (book metadata, else the device
locale) and lists only its voices, with no way to change it; the
hyphenation fallback made the same assumption. Adds a Book language
setting to the in-book options (This book): Auto shows whether it comes
from the book or the device, and an override is remembered per book,
sent to the EPUB navigator for hyphenation and to the read-aloud
navigator for voices, and applied live during playback.

Tests: JVM resolution and option rules; on-device voice lists with the
system speech engine (Auto, live switch, start in override), page
language and persistence across recreation, and the picker UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After recreation (rotation, fold, process death) the reader followed the
saved read-aloud position even with no read-aloud session running. That
position is where listening last stopped, often far behind where the
book was read since, and the jump was then saved and synced. Only a live
session for this reader is followed now; the saved position remains the
Keep listening start point.

Test: instrumented regression (red before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant