Skip to content

Reader overhaul: native rendering line, design-handoff P0+P1, perf fixes#1

Merged
DaviRain-Su merged 24 commits into
mainfrom
reader-native-overhaul
Jun 11, 2026
Merged

Reader overhaul: native rendering line, design-handoff P0+P1, perf fixes#1
DaviRain-Su merged 24 commits into
mainfrom
reader-native-overhaul

Conversation

@DaviRain-Su

Copy link
Copy Markdown
Owner

Summary

24 commits taking the reader from the WKWebView line to a fully native, design-handoff-aligned experience on Mac and iOS.

Native rendering line

  • Native EPUB selection/highlights with exact UTF-16 spans; cross-paragraph 跨段 selection sheet; the dead WebView/JS bridge (~1200 lines) removed
  • 微信读书-style page turning on iOS (TextKit pagination, swipe/edge-tap) and macOS (arrow keys / space / edge clicks); 上下滚动 stays switchable
  • EPUB parser covers bare-div text, tables, footnote asides, code blocks, nested lists, figcaptions

Design handoff P0 (from the claude.ai/design bundle)

  • Tokens aligned (vermilion #A93E22, gold #E5C55E); the spec's four themes 纸白/暖纸/夜读/墨黑; body-font choices 衬线/宋体/楷体/黑体
  • Highlights become underline washes (底线染色) in the 金/朱/青 trio; 划词菜单 leads with non-AI actions (color dots, 复制, 词典, 跨段), AI folds behind 朱 ›
  • Mac immersion mode (chrome fades after 2.5s mouse rest)
  • 目录/书签/搜索 drawer: Bookmark model + ⌘D toggle, full-text search with unread hits folded (防剧透)
  • Provider registry: per-feature routing (翻译/回顾/伴读/词汇) + cloud→local fallback chain, 按功能分配 UI

Design handoff P1

  • 阅读统计: active-time metering (idle gaps dropped), week/month charts, streak, measured speed → per-book remaining time
  • 朗读: sentence follow-along highlight, resume-from-position, 0.75–1.5× rate, 连读 auto-next-chapter
  • 导出: highlights/notes/bookmarks as Markdown/plain with empty:// deep links
  • 繁体显示 (system ICU, render-layer); PDF zoom memory, night inversion, Mac 双页

Reliability & performance

  • Translation gaps retry and backfill instead of staying blank; pretranslation is cloud-only (on-device stays per-viewport)
  • Mouse moves no longer re-parse the chapter (timers in reference boxes); chapter parse/spans memoized
  • Unit-test host boots an ephemeral container — tests never touch real stores
  • 151 → 170+ unit tests, all green (serial); UI smoke for page turning

Known deferred

  • 竖排 vertical-rl (needs a CoreText pass), PDF auto-crop, empty:// scheme registration for click-through

🤖 Generated with Claude Code

DaviRain-Su and others added 24 commits June 11, 2026 16:47
The highlight sheet can now write/edit notes inline (trimmed, clearable)
and jumps back to the stored UTF-16 anchor instead of the chapter top;
the Mac reader top bar surfaces the same sheet.

Native reader: long-chapter resume lands inside the matched text block
at its stored UTF-16 progress instead of the block top, and chapter
images decode on a utility-priority task (cancelled when scrolled away)
instead of blocking the main thread.

Work authored by the concurrent workbench session; committed after a
full green EmptyTests run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reader docs still described the retired WKWebView pipeline: README
claimed CSS-columns pagination and 108 tests; ARCHITECTURE said CloudKit
was disabled, utf16Offset was never reported, and listed PDF reading,
flashcard UI, and iOS notes as missing.

Now documented as built: the native SwiftUI rendering path (block model,
exact UTF-16 spans, cross-paragraph selection sheet, highlight notes +
exact jump-back), CloudKit .automatic with the local-only fallback,
ParagraphTranslation/StudyCardEntry in the store tables, the two cloud
standards (OpenAI-compatible + Anthropic Messages), the reading agent
loop, language-aware embeddings, pretranslation cache, the current iOS
IA, deterministic screenshot smoke tests, and a refreshed known-
limitations table. Test count updated to 144/144.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mac now honors -ScreenshotSeed like iOS (seeds the 思维之书 demo book at
launch), and the four screenshot UI tests drive seeding/opening through
launch arguments instead of depending on whatever book happened to be in
the library — captures are reproducible on a clean machine.

Work authored by the concurrent workbench session; committed after its
verification run and a full green EmptyTests build covering these files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real books rarely stick to p/h1-h6/blockquote/li, so the native parser
now also understands:

- bare text in div/section/article (many EPUBs paragraph with <div>s) —
  collected as implicit paragraphs at container boundaries, <br> splits,
  source-formatting newlines don't
- <table> — caption + th/td rows parsed into a .table block, rendered
  as a bordered serif grid with horizontal scroll
- footnote asides (epub:type *note) — paragraphs inside render as
  distinct 注-badged small-type blocks, still translatable
- <pre> — line structure and indentation preserved, rendered selectable
  in monospace on a card background
- nested ul/ol — indent per level, ordered lists get 1. 2. markers
- figure/figcaption — captions attach to their image and display under
  the loaded picture; missing images now say 插图缺失 instead of an
  eternal generic placeholder; dt/dd parse as paragraphs

Six new parser fixtures cover each shape; 150 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 「译」 toggle becomes a three-mode menu (原文 / 双语对照 / 导读,
persisted across launches) matching the Mac reader's modes. Visible
paragraphs resolve through the same kind-aware persistent cache, and
results that land after a mid-flight mode switch are cached but never
painted into the wrong mode.

双语对照 now pretranslates the current and next two chapters in the
background on iPhone/iPad too — same TranslationStore pipeline and
Chapter.pretranslatedAt marker as the Mac reader, so the two platforms
share one cache per book. 导读 stays per-viewport by design.

Reader font size and line spacing persist via AppStorage on both
platforms (committed for Mac in 9811950 alongside the smoke-test ids).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The coverage artifact was committed back in 37e27f6 and kept showing as
modified after every local test run; *.profraw is already gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`fastlane/test_output*.profraw` never matched anything, which is how
default.profraw kept reappearing after test runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five canvas themes in the 朱批 voice — 纸色 (follows the shell
light/dark), 月白, 米黄, 竹青 eye-care green, 玄夜 night — applied to
the whole reading surface (top bar, text, sheets) via a palette
override, with theme-aware ink colors in the native text views. Four
body fonts: 衬线 (system serif, as shipped), 宋体 Songti SC, 楷体
Kaiti SC, 黑体 system sans, with bold-trait and unknown-family
fallbacks.

阅读设置 gains font chips and theme swatches next to the existing
size/spacing sliders; all choices persist via AppStorage and apply to
Mac and iOS alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
左右翻页 is now the iOS default: the chapter lays out into real
TextKit pages (one NSTextStorage, a text container per page) and the
reader swipes or edge-taps between them — left quarter back, right
quarter forward, center toggles the chrome; crossing the first/last
page changes chapter. Selection, exact-range highlights, 双语/导读
notes, resume position and 还剩 N 页 all ride the same chapter-offset
pipeline, and re-layout (notes arriving, font/theme changes, rotation)
re-anchors to the page being read. 上下滚动 stays available in 阅读设置.

Two jank fixes behind the complaint that reading stutters:
- visible-paragraph reports fired on every scroll frame, each doing
  translation-cache fetches and chapter-text position math on the main
  thread — they now wait for a 180ms scroll settle (Mac + iOS)
- whole-chapter background pretranslation saturated the device when
  the model runs locally — cache-ahead is now cloud-only; on-device
  translates just the visible paragraphs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Selection drags fired a callback per character; a 180ms
SelectionChangeDebouncer coalesces them on both platforms, and inline
note lookups go through an index dictionary instead of a linear scan
per paragraph row.

Work authored by the concurrent workbench session; committed after a
full green EmptyTests run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TextKit pager goes cross-platform: macOS reads in real pages too
(arrow keys / space / edge clicks / always-on chevrons, 700pt column),
default on, switchable back to scrolling in 阅读设置 — whose entry
button had been lost from both reader top bars and is restored.

Cached-translation gaps stop being permanent: empty/errored results
retry while the paragraph is on screen (capped at 3 per visit),
pretranslation re-checks stamped chapters and backfills holes, and the
paged reader shows quiet ⟳ 生成中 / 暂不可用 lines instead of nothing.
iOS paged pages rebuilt as independent text views (fixes the broken
first cut), with taps listened on the text view itself; the demo book
grows long enough to paginate and a UI smoke test swipes across the
chapter boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…菜单, Mac immersion

From the claude.ai/design handoff bundle (开发 Handoff §1/§3 + 视觉精修):

- Tokens: vermilion unified to #A93E22 (light) per the spec, highlight
  gold to #E5C55E
- Reader themes are now the spec's four — 纸白 (follows the shell) /
  暖纸 EFE3CC / 夜读 2A241C / 墨黑 0D0C0A — with per-theme body ink
- Highlights render as a thick underline wash (底线染色) instead of a
  background block, in the design trio 金/朱/青 picked straight from
  the selection menu's color dots; HighlightColor gains
  vermilion/celadon cases, legacy values still render
- 划词菜单 leads with non-AI actions on both platforms — color dots,
  复制, 词典 (system dictionary: dict:// on Mac,
  UIReferenceLibraryViewController sheet on iOS), 跨段 — and the AI
  verbs fold behind a trailing 朱 › group
- Mac immersion: the chrome fades after the mouse rests 2.5s and wakes
  on movement; open overlays pin it visible

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the design handoff's 目录·书签·搜索 spec:

- Bookmark model (synced store, cascade inverse on Book) with a
  BookmarkStore whose ⌘D-style toggle removes the existing bookmark
  when one already sits within ~a page of the position
- The Mac in-reader panel grows 目录 / 书签 / 搜索 tabs: bookmarks
  list with jump-to-anchor and delete, and local full-text search
  whose unread-chapter hits stay folded behind a count until the
  reader explicitly opens them (防剧透 extends to non-AI features)
- Reader top bar gains the bookmark toggle (⌘D), vermilion-filled
  when the current position is bookmarked
- Tests: toggle semantics, reading-order sort, exact UTF-16 hit
  offsets, spoiler grouping, short-query guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The handoff's last P0 item: AIProviderRegistry upgrades the single
mode/protocol choice to a provider list (On-Device Apple + the linked
cloud entry mirroring the legacy config) with per-feature routes —
翻译·导读 / 回顾·概览 / 伴读对话 / 词汇 each pick their provider, and
resolution walks routed → default → local, returning the routed
provider's unavailability reason when nothing can serve (features
degrade silently; reading never blocks).

All 19 AI call sites now resolve through their feature route; the
legacy AIProviderSettings stays as the editable cloud config and the
registry re-syncs from it on every load, so existing setups migrate
with zero breakage. AI 状态 gains the prototype's 按功能分配 capsules
(click to cycle providers, unset = 默认).

Tests: legacy migration, route persistence, config-edit reflection,
dangling-default recovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The handoff's 统计口径: time only counts between real scroll/page
activity pings (gaps cap at 90s — an open book on the desk isn't
reading), accumulated into ReadingSession.activeSeconds; legacy rows
fall back to wall-clock capped at two hours.

ReadingStatsStore aggregates locally: zero-filled daily minutes for
week/month charts, a streak that survives an unread "today", a rolling
chars-per-active-minute speed from recent same-chapter sessions, and a
per-book remaining-time estimate from that measured speed.

ReadingStatsView (shared) draws the bars, streak/today/speed tiles and
the 在读剩余 list — entries from the Mac library header (统计) and the
iOS library greeting row.

Also: the unit-test host now boots an ephemeral container — tests must
never touch (or contend with a running app for) the real stores, which
was crashing parallel test clones at 0.000s all evening. Full suite
verified green serially; parallel-clone flake tracked separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The aloud bar grows up per the handoff: chapter text splits into
sentences (CJK/Latin terminators + newlines, exact UTF-16 ranges kept)
and the queue speaks them one by one, publishing the current range so
both readers paint a vermilion follow-along underline that walks the
text. 朗读 now starts from the sentence under the reading position
instead of the chapter's first 800 characters; a selection still reads
just the selection.

The bar gains a rate button cycling 0.75 / 1.0 / 1.25 / 1.5× and a
连读 toggle that rolls into the next chapter when the queue finishes.
Language auto-detects (CJK → zh-CN voice). Tests cover range-exact
splitting, whitespace fragments, position→sentence lookup, and
language detection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BookExporter renders a book's highlights (with their notes) and
bookmarks grouped by chapter, as Markdown (quote blocks, chapter
headings, link lines) or plain text. Every entry carries an
empty://book/<id>?c=<chapter>&o=<offset> reference back to its exact
passage — EmptyDeepLink builds and parses them (scheme registration
for click-through navigation is tracked separately).

BookExportView (shared, opened from the 高亮·朱批 list): 高亮/批注/书签
toggles and a format switch re-render the preview live; one tap copies
the whole document. Tests cover the deep-link round trip, foreign-URL
rejection, markdown completeness, and toggle pruning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t, 双页

繁体显示: render-layer simplified→traditional via the system ICU
transliterator (Hans-Hant) — no bundled word tables, fully offline,
source files untouched. Converted chapters cache per index; highlights
and selections operate in the converted space so anchors stay
self-consistent. Toggle lives in 阅读设置 (EPUB only).

PDF per the handoff: per-book zoom memory (scale factor persisted and
restored), 夜间反色 as hue-rotated inversion so colors keep their
identity, and a Mac 单页⇄双页 spread switch. All three toggles join the
settings panel's new 繁简·PDF row.

Deferred with notes: vertical-rl 竖排 (no native UITextView support —
needs a CoreText pass) and PDF auto-crop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The immersion timer lived in @State, so re-arming it on every
continuous-hover event invalidated the whole reader view — and reader
init re-parses the chapter XHTML and re-resolves every block's UTF-16
span. Every mouse movement re-parsed the book; everything felt slow.

- Timers move into reference boxes (TaskBox): re-arming mutates a
  class, SwiftUI doesn't re-render. Same for the scroll-settle timer
  (was invalidating per scroll frame) and the 繁体 cache (which also
  mutated @State during body evaluation).
- NativeChapterParser memoizes documents and resolved spans in
  NSCaches keyed by href + length + content tail, so legitimate
  re-renders (translations arriving, page flips, theme changes) reuse
  the parse instead of paying it again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DaviRain-Su DaviRain-Su merged commit 3edf2ef into main Jun 11, 2026
1 of 2 checks passed
@DaviRain-Su DaviRain-Su deleted the reader-native-overhaul branch June 11, 2026 15:19
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