Commit Graph

49 Commits

Author SHA1 Message Date
Huang Xin c5304cd46c fix(reader): turn pages horizontally for vertical-rl books (#624) (#4899)
Vertical-rl books paged along the vertical scroll axis: page turns slid
up/down and only vertical swipes turned pages. Vertical books read with
right-to-left page progression, so page turns now work horizontally,
matching printed vertical books:

- Swipes track the finger: the page follows a horizontal drag and the
  release commits the turn (past half a page width or a flick in the
  drag direction) or settles the page back.
- Arrow keys, tap zones, and the wheel follow the same rtl mapping that
  horizontal-rtl books use.
- Animated turns run a two-phase horizontal slide that continues from
  the dragged offset: the outgoing page exits along the page
  progression, the scroll jumps while off-screen, and the incoming page
  follows in from the opposite edge. A single-phase push is impossible
  because CSS multicol stacks vertical-rl pages along the vertical
  scroll axis inside one iframe, so the outgoing and incoming page can
  never be on screen side by side.
- With animation disabled (or e-ink), turns swap instantly as before.

The paginator changes live in the foliate-js submodule; this bumps the
pointer and adds browser tests with a vertical-rl EPUB fixture covering
direction detection, drag tracking, drag revert, horizontal swipe
mapping in both directions, the legacy vertical swipe, the horizontal
slide animation, the instant non-animated swap, and the unchanged
horizontal-ltr swipe behavior.

Fixes #624

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:51:36 +02:00
Huang Xin 17e60f1e49 fix(reader): fix fixed-layout spread spine seam and zoomed-out blank page (#4857) (#4873)
Bump foliate-js with two fixed-layout (EPUB and PDF) two-page spread fixes:

- Spine seam: overlap the two pages by one device pixel to hide the 1px white
  seam that appeared at the spine at a fractional devicePixelRatio (e.g.
  Windows 150% display scale).
- Zoomed-out blank page: keep non-PDF pages in block flow below 100% zoom; the
  PDF-only zoom-out centering was pushing the un-scaled iframe out of view and
  blanking the page.

Adds a unit test for the computeSpreadSpineOverlap helper.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:38:00 +02:00
Huang Xin f8916e128e fix(reader): smooth pinch-zoom and pan for scrolled-mode PDF (#4817)
Bumps foliate-js to readest/foliate-js#43. In scrolled-mode PDF the page now zooms live under a pinch and commits without a layout shift (the inter-page gap scales with the zoom so the committed layout matches the transform-scaled preview, and the centre page is restored to its pre-commit on-screen rect), a page zoomed wider than the viewport is pannable horizontally, and the page iframes stay interactive when idle so native text selection keeps working. readest already drives the renderer's pinchZoom on a two-finger gesture, so the only reader-side change is the submodule bump plus a unit test for the new scroll pinch transform.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:15:33 +02:00
Huang Xin 580c5e5deb fix(reader): eliminate PDF scrolled-mode rendering lag on mobile (#4795) (#4813)
PDF pages rendered blank while scrolling in scrolled mode (#4795,
resurfacing #4031). On-device profiling showed each page takes hundreds
of ms to render while the preload margin gave only about half a page of
lead, and loads were unbounded and unprioritized.

Bump the foliate-js submodule to widen the scrolled-mode preload margin
and drive page loading through a bounded, viewport-prioritized scheduler
(readest/foliate-js#40). Adds unit coverage for the new planScrollModePages
scheduler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:02:16 +02:00
Huang Xin a0227f98e2 perf(reader): stop per-frame background reflow on swipe page turns (#4785) (#4814)
Fixes readest/readest#4785

Swipe page turns dropped frames, worst when crossing .xhtml section
boundaries. The paginator's #replaceBackground rebuilt its whole paint
context every animation frame (a getComputedStyle plus one
getBoundingClientRect per rendered view), and that per-frame cost scales
with the number of loaded views, which peaks at a boundary where adjacent
sections are preloaded.

Bumps the foliate-js submodule to 15fc999 (readest/foliate-js#41) to
snapshot the paint context once per gesture and reuse it on every frame,
and to defer the heavy mid-drag section preload off the active drag. The
bump also advances foliate-js to current main, picking up the
gpu-composite page-turn opt-in (readest/foliate-js#39).

Adds a real-browser test that drives an animated turn and a synthetic drag
and asserts the section <html> computed style is read a small constant
number of times instead of once per frame (snap 39 -> <=3, drag 7 -> <=1).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 04:26:37 +02:00
Huang Xin 97868f0486 fix(reader): keep negative table margins from clipping wrapped layout tables (#4439) (#4808)
A decorative table-of-contents page lays out as nested tables where the inner
table pulls itself up with a negative top margin and the CONTENTS heading uses
line-height:1em. Since #4400 wraps every table in a `.scroll-wrapper`
(overflow:auto), that negative margin bled the heading above the wrapper's clip
box and overflow cut off the top half of its glyphs.

Hoist any negative margins from the wrapped element onto the wrapper and zero
them on the element: the box stays in place, the element sits flush inside it so
overflow cannot clip it, and scrollWidth is no longer inflated by the margin so a
table that actually fits still gets marked fit. Positive and auto margins are
left alone, so an over-wide table still scrolls and a centered table stays
centered.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:25:21 +02:00
Huang Xin 428168ac91 fix(reader): show the centred section's chapter title in scrolled mode (#4739)
In scrolled mode the header chapter title was wrong while transitioning
between sections, while paginated mode was correct (#4436). foliate-js
#getVisibleRange returned the first overlapping view (topmost in scroll
order), so when the tail of one section was a thin sliver at the top of
the viewport and the next section occupied the centre and most of the
screen, the relocate event reported the sliver's section — and its title
lagged behind what the reader was reading.

Bump foliate-js to prefer the view covering the viewport centre
(readest/foliate-js#37) and add a browser-lane regression test that
scrolls a sliver of section K to the top with K+1 across the centre and
asserts the relocate index is K+1.

Fixes #4436

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 01:56:10 +02:00
Huang Xin 1b44b95d3a fix(reader): smooth single-notch wheel scroll over PDF pages in scrolled mode (#4727) (#4732)
Bumps foliate-js to drop the redundant manual `scrollBy` the scrolled-mode
page iframes ran on every wheel event. Because those iframes are
`scrolling="no"`, the browser already chains the wheel to the host scroller
natively; the extra scrollBy stacked on top, so wheeling over a page moved
it ~2x as far in an instant lurch while the margins scrolled smoothly by one
notch. Native scroll-chaining now provides the single smooth scroll over both
the page and the margins.

Adds a browser-lane regression test that mounts the real <foliate-fxl>
renderer in scrolled mode and asserts a wheel over a page does not
programmatically move the host scroller.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:17:24 +02:00
Huang Xin a9c0f3d46d fix(reader): remove 1px white seam in PDF spread at fractional DPI (#4587) (#4713)
In a PDF two-page spread at a fractional devicePixelRatio (Windows display
scale 150% -> dpr 1.5), a one-pixel white bar appeared at the spine on certain
zoom levels. foliate-js' pdf.js sized the page canvas only via its bitmap, so
the fractional viewport width was truncated and the canvas rendered up to ~1
device pixel narrower than the page box, exposing the background at the spine.

Bump the foliate-js submodule to the fix (readest/foliate-js#35) which pins an
explicit canvas CSS size to the un-truncated viewport dimensions, and add a
regression test that drives render() at dpr 1.5 and asserts the canvas fills
its box exactly.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:20:28 +02:00
Huang Xin 2153f7cc0c fix(reader): reset scroll to top on paginated fit-width page turn (#4683) (#4695)
In paginated fixed-layout mode (PDF / fixed-layout EPUB) with fit-width zoom, a
page taller than the viewport makes the renderer host scroll vertically. Turning
the page kept the previous page's vertical offset, so the next page opened
scrolled to the end instead of the top. The bug only shows on WebKit
(Linux/iOS/macOS), which preserves the scroll offset across the page content
swap; Blink (Android/Chrome) resets it to zero.

Bump foliate-js to the fix (readest/foliate-js#34): reset scrollTop on a page
turn only. Add a unit test for the new computePaginatedScroll helper.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:46:33 +02:00
Huang Xin a9526377a2 fix(reader): stretch Duokan fullscreen cover to fill the page (#4679)
Bump foliate-js so paginated Duokan full-page covers
(data-duokan-page-fullscreen) render with object-fit: fill instead of
contain. The cover now fills the whole page, distorting to fit when the
aspect ratio differs, matching Duokan's native full-page render. Adds a
browser test asserting the fullscreen cover computes object-fit: fill.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 05:33:47 +02:00
Huang Xin 0ab8f6042f fix(reader): keep cover background-image visible under a texture (#4675)
Bump foliate-js to the textureAwareBackground fix and add a regression
test. A cover page that paints its image via a body background-image
leaves background-color transparent, so the computed background shorthand
starts with "rgba(0, 0, 0, 0)" even though a real image follows. The
paginator misclassified it as transparent and, with a background texture
active (e.g. parchment), dropped the page background so the texture showed
on the first page instead of the cover. Verified on Android WebView.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:13:28 +02:00
Huang Xin 6caa376f82 feat(reader): Webtoon Mode seamless continuous scroll for image books (#3647) (#4662)
* feat(reader): make fixed-layout scroll gap configurable (foliate-js bump) (#3647)

* feat(reader): add webtoonMode view setting + scroll-gap helper (#3647)

* feat(reader): Webtoon Mode toggle in the fixed-layout view menu (#3647)

* feat(reader): apply Webtoon Mode gap on fixed-layout book open (#3647)

* fix(reader): clear Webtoon Mode + reset gap when Shift+J leaves scrolled (#3647)

* chore(i18n): translate Webtoon Mode string across locales (#3647)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(deps): bump foliate-js to merged readest/foliate-js#30 (#3647)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:37:37 +02:00
Huang Xin e327d0c992 feat(tts): reuse the speaking session across paragraph & RSVP modes (#4657)
Switching into Paragraph or RSVP mode while TTS is already playing now
syncs to the live session instead of forcing a stop + restart inside the
mode. Bundles several related TTS fixes uncovered along the way.

Session reuse (enter from normal mode):
- TTSController.redispatchPosition() re-emits the current position on the
  canonical tts-position signal with a fresh sequence.
- useTTSControl answers a new tts-sync-request by replaying the current
  position then playback state (position-first so RSVP's paused handler
  can't discard it).
- Paragraph & RSVP engage following on entry and dispatch the request;
  no-op when no session exists.

RSVP refinements:
- Reusing a session skips the start dialog and the get-ready countdown
  (starts externally driven); gated on a live tts-playback-state signal
  so the countdown can't flash.
- Stopping TTS now pauses RSVP instead of resuming its own pacing.

Word-sync fixes:
- rangeTextExcludingInert honours the range offsets inside a single text
  node, fixing word-highlight drift on middle sentences of single-<span>
  paragraphs (Edge word highlighting).
- foliate-js TTS.from() starts at the sentence containing the selection,
  not the next one (submodule bump).
- Selecting a word and starting TTS now clears the selection.
- Dev-only [TTS] word-sync trace (stripped from production builds).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 07:47:29 +02:00
Huang Xin af587b1a41 fix(metadata): parse FB2 series from title-info sequence (#4646) (#4649)
FB2 stores series info as `<sequence name="…" number="…"/>` inside
`<title-info>`, but the foliate-js FB2 parser never read it, so
`belongsTo.series` was always empty and the series name/index never
surfaced in the library or book details. Refresh-metadata and
re-import didn't help since they share the same parser path.

Bumps the foliate-js submodule to pull in the `<sequence>` parsing
(readest/foliate-js#28) and adds a regression test + fixture.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:12:59 +02:00
Huang Xin bcd9ed724b fix(reader): paginate inline-block-wrapped chapters instead of clipping them (#4641)
* fix(reader): paginate inline-block-wrapped chapters instead of clipping them (readest/foliate-js#27)

Some EPUBs wrap a large chunk of chapter content in a div the stylesheet
declares as `display: inline-block`. Atomic inline-level boxes can't fragment
across CSS columns, so in paginated mode the tall box overflows the page
vertically and every column past the first is clipped — the chapter jumps
straight to its "Reference materials", silently skipping a large middle
section, while the counter reads "1 page left in chapter".

Bumps the foliate-js submodule with #demoteUnfragmentableBoxes (demotes
over-tall atomic-inline boxes to their fragmentable block equivalents in
column mode) and adds a browser test + repro EPUB fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(deps): bump foliate-js submodule to merged main (#27)

Re-point packages/foliate-js from the PR-branch commit to the squash-merged
main SHA now that readest/foliate-js#27 has landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:15:22 +02:00
Huang Xin 8bcb9f9b2a feat(wordlens): trim hints to first sense + suppress known derivations (#4635)
* refactor(wordlens): rename ww-gloss CSS class to wl-gloss

Completes the Word Wise → Word Lens rename (#4633) for the gloss ruby class —
the 'ww' shorthand was missed. Renamed consistently across the apply site
(GLOSS_CLASS), the CSS rules in style.ts, the tap hit-test in
iframeEventHandlers, and the browser tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(wordlens): trim hints to first sense + suppress known derivations

Runtime, best-effort gloss-quality pass over the shipped en-zh pack (no
regeneration):

- cleanGloss: strip leading POS tags (now incl. 6-letter `interj.`) and keep
  only the first sense, so hints stay short — "Ahem" shows 呃哼, not
  "interj. 呃哼"; multi-sense entries collapse to their first sense.
- Derivational reduction (English source only): a would-be-glossed word inherits
  a known base form's lower rank when the base exists in the pack AND their
  glosses share meaning, so lazily/shyly/sorrowful/downwards/inwards stop being
  hinted once lazy/shy/sorrow/… are known. Drifted forms keep their own rank
  because their gloss doesn't overlap the base (hardly≠hard, lately≠late).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(wordlens): note hint-quality layer + wl-gloss in agent memory

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:46:54 +02:00
Huang Xin c2ac207945 refactor(wordlens): rename "Word Wise" to "Word Lens" (#4633)
"Word Wise" is a Kindle trademark, so rename the inline-gloss feature to
"Word Lens" throughout the product.

- User-facing strings → "Word Lens" across all 34 locales; brand translated
  for Chinese (zh-CN 单词透镜, zh-TW 單詞透鏡) and German (Word-Lens-Daten).
- Code identifiers: WordWise→WordLens, wordWise→wordLens, WORD_WISE→WORD_LENS.
- Files/dirs: src/services/wordwise→wordlens, WordWisePanel→WordLensPanel,
  wordwise{Ruby,Section}.ts, build/sync scripts, test dirs/fixtures,
  data/wordwise→data/wordlens.
- Storage paths: CDN base, R2 key, on-device cache dir, WORDLENS_R2_BUCKET env,
  pnpm wordlens:{manifest,sync}. manifest.json is path-agnostic so its
  sha256/bytes stay valid (verified).
- biome.json: point the formatter-ignore at data/wordlens so the generated
  one-line gloss packs aren't pretty-printed on commit.

Migration notes:
- Re-run `pnpm wordlens:sync` to upload packs to cdn.readest.com/wordlens/.
- Persisted view-settings keys renamed (wordWiseEnabled/Level/HintLang and
  wordWiseAutoDownload) — saved values reset to defaults once on upgrade.
- Cached packs under the old Data/wordwise/ orphan (harmless re-download).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:09:29 +02:00
Huang Xin 4908245042 feat(reader): Word Wise inline vocabulary hints (#4589)
* feat(reader): Word Wise — inline native-language vocabulary hints

Kindle-style Word Wise: a short native-language gloss renders above difficult words
as you read (always-on ruby), gated by a CEFR vocabulary-level slider (A1–C2);
tapping a glossed word opens the existing dictionary.

- Pipeline: CEFR→frequency-rank difficulty, inflection-aware gloss index, pure
  offset-aware planner (EN regex + jieba for CJK).
- Rendering: <ruby cfi-skip>…<rt cfi-inert> injected per occurrence — CFI-transparent
  (verified), so highlights/bookmarks/progress are unaffected; kept out of TTS word
  offsets and find-in-book.
- Delivery: gloss packs are version-controlled in data/wordwise/, mirrored to R2, and
  downloaded on demand into local storage (sha-verified, single-flight) when enabled.
- Settings: a Word Wise sub-page under Settings → Language (enable, level, hint
  language, per-pack download/manage, auto-download toggle).
- Build tooling: scripts/build-wordwise-data.mjs (ECDICT / CC-CEDICT+HSK / WikDict +
  FrequencyWords, with lemmatization) and scripts/sync-wordwise-r2.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* data(wordwise): bundled gloss packs + manifest + attribution

13 frequency-trimmed gloss packs (en↔中文 + es/fr/de/pt/it/ru↔en, ~19 MB) generated
by build-wordwise-data.mjs from ECDICT (MIT), CC-CEDICT + HSK, and WikDict +
FrequencyWords (CC-BY-SA). Source of truth, mirrored to the CDN via `pnpm wordwise:sync`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:56:00 +02:00
Huang Xin 12ac7ae6c0 fix(reader): draw annotation highlights over bullet lists (#4552)
Highlights spanning paragraphs and a bullet list painted the
paragraphs but not the list items: the overlayer split ranges with a
hard-coded 'p, h1, h2, h3, h4' selector before collecting client
rects, so li/blockquote/td text fell into no sub-range and produced
no SVG rects. Bump foliate-js to split by text nodes (plus img/svg)
instead, which covers every block type while still excluding the
block border boxes that over-highlight blank space.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:34:34 +02:00
Huang Xin c2bbb6119a fix(reader): keep paginated page background inside its column (#4394) (#4429)
On a multi-column spread a coloured page's background bled into the outer margin gutter (the --_outer-min track) while an adjacent transparent/image page did not, shifting cover/title spreads off-centre. Bump foliate-js to clamp each background segment to its column instead of stretching into the gutter, keeping the symmetric margins intact. Update the computeBackgroundSegments regression tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:48:56 +02:00
Huang Xin fe7fe25482 fix(reader): show background texture in paginated mode (#4399) (#4417)
The background texture (mounted on the reader container as
`.foliate-viewer::before`) showed in scrolled mode but was absent in
paginated mode: the paginator painted an opaque #background container
over it. Bump foliate-js to leave that container transparent under a
texture, and add a regression test for the shared textureAwareBackground
helper.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:54:59 +02:00
Huang Xin 3a81e09911 fix(reader): scroll oversized blocks in-place instead of turning the page (#4400) (#4415)
Wide or tall tables, code blocks and display equations overflowed the reading
column and a scroll gesture over them turned the page instead of scrolling the
content (#4400).

- Wrap tables and display equations in a horizontally/vertically scrollable
  container; route touch + wheel along the box's scrollable axis so it scrolls
  the box and never turns the page, even at the edge (both axes).
- A box that fits its column is marked fit (overflow:visible) so it never clips
  or captures gestures; the fit decision is measured once after layout via a
  self-disconnecting ResizeObserver, so it never relayerizes during a page turn.
- The scroll wrapper carries a new cfi-skip attribute that makes it transparent
  to CFI: epubcfi.js hoists a cfi-skip node's children into its parent (unlike
  cfi-inert which drops the subtree), and xcfi.ts mirrors this for CFI<->XPointer
  so existing highlights, bookmarks and KOSync positions inside a wrapped table
  or equation still resolve. The sanitizer whitelists cfi-skip.
- Bump foliate-js submodule (cfi-skip support + raf fallback for large sections).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:45:18 +02:00
Jesus Eduardo Medina Gallo 458ad7510c fix(reader): scroll wide EPUB tables horizontally (#4391)
* fix(reader): scroll wide tables horizontally instead of scaling

Why:
- Wide EPUB tables (e.g. many columns without cell widths) overflowed the
  page because CSS scale only applied when widths were known.
- Paginated mode stole horizontal swipes for page turns over table content.

Refs:
- Replaces transform-based applyTableStyle scaling with a scroll wrapper
  and capture-phase touch routing (same pattern as gesture brightness).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(reader): keep wheel/trackpad table scrolling from turning the page

Horizontal scrolling of a wide table in paginated mode also turned the page:

- applyTableTouchScroll only routed touch events. Trackpad/mouse wheel
  (readest forwards iframe wheel -> 'iframe-wheel' -> pagination) was
  never intercepted, so a horizontal wheel both scrolled the table and
  flipped the page.
- findWrapper used `instanceof Element`, which is always false for iframe
  event targets because this module runs in the top-window realm. The
  touch routing therefore never fired either.

Add a capture-phase wheel handler that consumes horizontal wheels over a
scrollable table -- including at the scroll edge, so the gesture (and
trackpad momentum) never chains into a page turn -- and make findWrapper
cross-realm safe via duck-typing on `closest`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reader): don't show a spurious scrollbar on layout tables

Wrapping every table for horizontal scrolling made tables that fit the
column (e.g. character/glossary layout tables) show a spurious horizontal
scrollbar, because the wrapper was always scrollable.

Now the wrapper clips (no scrollbar) for a table that fits within a few px
of the column, and a ResizeObserver re-evaluates this as the column width
settles. A table genuinely wider than the column always scrolls and is
never clipped; one that wraps to fit shows no scrollbar. Touch/wheel
routing engages only scrollable wrappers.

Add a Chromium browser test over sample-table-layout.epub (layout tables
must not scroll) and sample-table-wide.epub (a too-wide table must scroll,
not clip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:42:53 +02:00
Huang Xin 9b4db44490 fix(pdf): ship jbig2.wasm so scanned PDFs render in packaged builds (#4382)
pdfjs-dist 5.7.x (bumped 5.4.530 -> 5.7.284 in #4143) moved the JBIG2
image decoder -- the codec used by virtually every black-and-white
*scanned* PDF -- from pure JS into a WebAssembly module the worker
fetches at runtime from `wasmUrl` (/vendor/pdfjs/). The `copy-pdfjs-wasm`
script only copied an explicit allow-list ({openjpeg.wasm,qcms_bg.wasm})
and silently dropped jbig2.wasm. cpx does not error on an empty glob, so
the missing decoder went unnoticed: scanned PDFs rendered blank (pages
still turned) in CI builds, while local `tauri build` reused a stale
pre-bump public/vendor copy and worked. Regression between 0.11.1 and
0.11.2.

Copy the whole wasm/ dir (mirrors the {cmaps,standard_fonts}/* fonts
pattern) so future decoders moving to wasm can't be dropped again, and
ship the *_nowasm_fallback.js files for graceful degradation.

Adds a regression test asserting every .wasm the bundled pdf.js
references is covered by copy-pdfjs-wasm.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 08:52:10 +02:00
Huang Xin de3e4b6d3c fix(reader): show Duokan fullscreen cover in scrolled mode (#4379) (#4381)
A cover with `data-duokan-page-fullscreen` on <html> renders in
paginated mode but is blank in scrolled mode; only the first cover is
affected, other images are fine in both modes.

The paginator's fullscreen branch pins such images with
position:absolute and height:100% and forces their ancestors to
height:100%. That fills the fixed-height page when columnized, but in
scrolled mode the container height is `auto`, so height:100% resolves
to 0 and the cover collapses out of view.

Bump foliate-js to gate the fullscreen treatment on column mode and
reset any stale absolute pinning when a fullscreen-cover doc is laid
out scrolled (so toggling paginated -> scrolled also recovers). Add a
browser regression test + repro-4379.epub fixture.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:52:42 +02:00
Huang Xin d0071a6bcb fix(sync,reader): discard malformed sync CFIs; fix swipe background flash (#4370)
sync: empty-start/end range CFIs left by the cfi-inert skip-link bug (e.g.
epubcfi(/6/24!/4,,/20/1:58)) resolve to a section-spanning range and navigate
to the wrong end of the section. Add isMalformedLocationCfi and discard such
locations on the cloud-sync receive path (useProgressSync) and the kosync push
path (useKOSync) so they can't move the reader or propagate to other devices.
foliate 569cc06 stops generating them but does not repair already-synced values.

reader: bump foliate-js to 167757a to fix the white<->black background flash
when swiping between differently-colored pages; add a regression test for the
sliding per-view background segments.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 21:02:52 +02:00
Huang Xin c23c21d37d fix(kosync): reflowable conflict comparison via local CFI; scrolled-mode + library fixes (#4367)
* feat(kosync): compare reflowable conflicts via locally-resolved CFI percentage

KOReader reports progress as a percentage from its own pagination, which isn't directly comparable to Readest's progress. For reflowable books, resolve the remote XPointer to a local CFI and compute the equivalent fraction (getRemoteLocalFraction), comparing that against the local percentage and falling back to the reported percentage only when it can't be resolved locally (non-XPointer progress or a missing section). The resolved fraction also drives the conflict-dialog remote preview so the shown value matches what was compared.

Loosen the conflict threshold to 0.01 when the remote progress was last pushed from this same device (remote.device_id === local deviceId), so sub-page drift between a push and the next pull doesn't prompt. Render sync percentages with 2 decimals via formatProgressPercentage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reader): correct scrolled-mode reopen drift over background-image sections

Bump the foliate-js submodule to include the scrolled-mode reopen drift fix for sections with background images, and add a browser regression test plus its EPUB fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(library): redirect to login on pull-to-refresh when signed out

Guard the pull-to-refresh handlers so an unauthenticated user is sent to the login screen instead of attempting a library pull and OPDS subscription check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(memory): add kosync conflict + toc/scrolled-restore notes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reader): prevent CFI crash on inert-only section bodies

Reopening/​paginating across a background-image or otherwise content-less section could crash with "Cannot destructure property 'nodeType' of 'param' as it is undefined" in foliate's fromRange, aborting the relocate so the reading position was never saved. Bumps the foliate-js submodule to 569cc06 (visible-range walker skips cfi-inert skip-links; isTextNode/isElementNode are null-safe) and adds a regression test reproducing the exact crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(kosync): keep auto-push working when a pull finds no real conflict

In the 'prompt' strategy, pullProgress set syncState to 'conflict' unconditionally on every pull that returned remote progress, even when promptedSync found no actual difference. Since auto-push only runs while 'synced', and a pull fires on every book-open and window re-activation, progress stopped being pushed. promptedSync now returns whether a real conflict was surfaced, and pullProgress only stays in 'conflict' for genuine conflicts (otherwise 'synced').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(settings): show most recent sync time and reorder settings tabs

Library settings menu now reports the latest of the book/config/note sync timestamps as "Synced …" instead of only the books timestamp. Reorder the settings tabs so Integrations precedes TTS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:07:29 +02:00
Huang Xin a848c142c8 test(reader): de-flake scrolled-mode backward-preload precondition (#4112) (#4354)
The two #4112 scrolled-mode preload browser tests asserted that the
previous-previous section (F-1) was not loaded after navigating to a section,
checking it after `waitForFillComplete()`.

The eager backward buffer (minPages) is suppressed while `#display` is
stabilizing, but pulls F-1 (and beyond) in on the first scroll events once the
fill settles. So the post-fill assertion raced the buffer: it passed locally but
failed intermittently on CI (loaded set [1,2,3,4,5]).

Assert the precondition the instant `#display` stabilizes — where the buffer is
provably suppressed and only the immediate-previous section is loaded — before
the fill settles. The eager buffer loading F-1 later is exactly what the second
test wants once it navigates back. Test-only; production behaviour is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:22:58 +02:00
Huang Xin 3c14d5a4b9 fix(reader): scrolled-mode prev-section preloading and nav drift (#4112) (#4349)
Bump foliate-js: compensate the scroll position when a previous section is prepended above the viewport, so the browser's scroll-anchoring suppression at scrollTop 0 no longer drifts the view into chapter n-1; preload the previous section on backward navigation and eagerly while scrolling toward the top; and stop the blank-screen flash on adjacent navigation in continuous scrolled mode.

Split paginator-multiview.browser.test.ts into paginator-scrolled and paginator-paginated, adding regression tests for the drift, previous-section preloading, the eager backward buffer, and the flash-free transition.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:16:25 +02:00
Huang Xin 1f5481c0e3 fix(fxl): align TTS highlight overlay with scaled iframe coords (#4324)
Non-PDF fixed-layout EPUBs visually scale the iframe via CSS transform
while keeping native dimensions inside, so getClientRects() returns
unscaled positions. The SVG overlayer was sized in CSS pixels with no
viewBox, leaving annotations and TTS highlights drawn at scale-1
displacement from the actual text.

Bumps foliate-js to set a matching viewBox on the overlayer SVG, and
adds a regression test covering spread/scroll/PDF/empty frames.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:19:27 +02:00
Huang Xin 49b171f5e5 fix(reader): restore right-column clicks and selection in dual-page mode (#4283)
* fix(reader): restore right-column clicks and selection in dual-page mode

Bumps foliate-js to readest/foliate-js@1ea2996, which stops marking
visible non-primary views as `inert` / `aria-hidden`. Adds a regression
test for the underlying visibility helper.

When a dual-page spread crosses a section boundary, the right column
lives in a non-primary view. The previous a11y sync set both `inert`
and `aria-hidden` on that view — `inert` blocked link clicks and text
selection in the visible column, and `aria-hidden` hid it from
assistive tech while sighted users could still read it. The fix drops
`inert` entirely (screen-reader swipe-next is already handled by
`aria-hidden`) and applies `aria-hidden` only to views whose bounding
rect lies outside the visible container.

Fixes #4243 (right-column links unclickable in dual-page mode).
Fixes #4259 (text selection fails when an image section is on the left).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(reader): update paginator-multiview a11y assertions for new visibility-based aria-hidden

Browser tests previously assumed every non-primary view carried both
`inert` and `aria-hidden`. The fix in the preceding commit drops `inert`
entirely and only `aria-hidden`s views that are off-screen. Update the
two assertions to:

- check that no wrapper ever has `inert`;
- require `aria-hidden="true"` only when the wrapper's bounding rect is
  fully outside the visible container, and require its absence when the
  wrapper overlaps the viewport (the regression case from #4243 / #4259).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:17:04 +02:00
Huang Xin 5e366018df fix(cbz): ComicInfo metadata + CBZ page count + WebDAV i18n (#4282)
* fix(cbz,i18n): ComicInfo metadata + CBZ page count + WebDAV i18n

Closes #4253 (ComicInfo.xml not read) and #4255 (CBZ shows "1 page left").

CBZ / ComicInfo (foliate-js submodule + Readest derivation):
- comic-book.js: find ComicInfo.xml in subdirectories too, parse
  description / subject / identifier / published / series fields
  beyond the prior name+position pair. Series Count populates the
  canonical `belongsTo.series.total`; no top-level duplication.
- bookService.ts / readerStore.ts: derive `metadata.seriesTotal`
  from `belongsTo.series.total` in parallel to the existing
  series / seriesIndex derivation.
- ProgressBar / FooterBar / DesktopFooterBar: drop the hard-coded
  `pagesLeft = 1` for fixed-layout books and compute it from
  `section.total - section.current`. FooterBar uses
  `FIXED_LAYOUT_FORMATS.has(bookFormat)` so CBZ picks `section`
  (correct image count) instead of `pageinfo` (locations).
- ProgressBar: switch the remaining-pages text to "in book" for
  fixed-layout titles (no chapter structure) and keep
  "in chapter" for reflowable books.

WebDAV refactor for translation coverage:
- WebDAVBrowsePane / SyncHistoryPanel called `t(...)` (passed as a
  prop) instead of `_(...)`. The i18next-scanner only looks for
  `_`, so ~53 strings were unreachable and shipped in English to
  every locale. Switched both components to call
  `useTranslation()` themselves; helpers that aren't React FCs
  take `_: TranslationFunc` so the scanner sees the literal calls.
- WebDAVClient.checkConnection now returns a `code` discriminator
  (`SERVER_URL_REQUIRED` / `AUTH_FAILED` / `ROOT_NOT_FOUND` /
  `UNEXPECTED_STATUS` / `NETWORK`); raw English `message` is
  reserved for the dev console. New `formatConnectError` and
  `formatSyncError` helpers in WebDAVForm translate via a switch
  where each branch is a literal `_('...')`. Same treatment for
  the sync-failure path that previously surfaced raw e.message.
- "Syncing 0 / {{total}}" is now parameterized as
  "Syncing {{n}} / {{total}}" with n=0 at startup so the digit
  formats naturally and the template can be reused mid-sync.
- "Cleanup · {{count}} book(s)" hard-coded options used unsupported
  ternary; rewrote as plural-aware key.

i18n scanner fix (i18next-scanner.config.cjs):
- vinyl-fs walked into directories whose names end in source-file
  extensions (Next.js route folder `runtime-config.js/`, Playwright
  screenshot folder `*.test.tsx/`) and crashed with EISDIR.
  Resolved by expanding globs via `fs.globSync` and filtering to
  files only before handing to the scanner.

TypeScript-syntax sites that broke esprima during extraction:
- WebDAVBrowsePane / WebDAVForm: `(e as Error).message` and
  `failed[0]!.title` inside `_(..., options)` arguments. Replaced
  with `e instanceof Error ? e.message : String(e)` and
  `failed[0]?.title ?? ''` — also runtime-safer.

User-facing em-dash cleanup:
- Removed em-dashes from translation keys across SyncHistoryPanel /
  WebDAVForm / WebDAVBrowsePane / SyncPassphraseSection / send/page /
  replicaCryptoMiddleware / AIPanel. Tagline in `layout.tsx` kept.

Locale translations:
- ~2400 translations applied across all 33 locales for the keys
  that were either newly extractable, freshly worded, or
  pre-existing but untranslated. Zero `__STRING_NOT_TRANSLATED__`
  remain after the run.

Misc:
- next.config.mjs: drop `eslint.ignoreDuringBuilds: true` so build
  runs the same lint as CI.
- Collection type: add `total?: string` for ComicInfo series count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(test): fix vitest invocation, run with 4 workers

`pnpm test:pr:web` was chaining `pnpm test -- --watch=false`, which
pnpm expanded into:

    dotenv -e .env -e .env.test.local -- vitest -- --watch=false

The second `--` made vitest treat `--watch=false` as a positional
file pattern, not a flag. Vitest then fell back to defaults (in CI's
non-TTY env that still meant a one-shot run, so the suite passed),
but the worker pool was effectively serialized for big chunks of the
243-file run — wall ~90 s on a 4-vCPU runner where the parallel-sum
of phases was ~236 s (≈2.6× effective parallelism).

Replace the chained pnpm invocation with a direct call to
`vitest run --maxWorkers=4`, matching the 4 vCPUs the GH Actions
ubuntu-latest runner provides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:59:29 +02:00
loveheaven 9f0aa2f55d fix(tests): materialize zip.js blob before wrapping in File for case-mismatch fixture (#4203)
The case-mismatch EPUB fixture builds an archive with @zip.js/zip.js' BlobWriter and then wraps the resulting Blob into a File:

  const blob = await writer.close();
  new File([blob], 'case-mismatch.epub', ...);

Under vitest's happy-dom/jsdom, the File/Blob polyfill does not correctly pull bytes out of a nested Blob part produced by zip.js. The outer File reports a non-zero size, but the bytes BlobReader sees in DocumentLoader (libs/document.ts: 'new BlobReader(this.file)') are not a valid ZIP — getEntries() yields nothing, open() falls through with book = null, and the test crashes at:

  TypeError: Cannot read properties of null (reading 'sections')

Materialize the zip bytes into a plain ArrayBuffer first, then construct the File from that. ArrayBuffer parts go through the polyfill cleanly because they don't require recursive Blob unwrapping, so zip.js reads a real archive and the test passes:

  const arrayBuffer = await blob.arrayBuffer();
  new File([arrayBuffer], 'case-mismatch.epub', ...);

This brings the fixture in line with the rest of the test suite (paginator-expand, page-progress-epub, toc-cfi-mapping, ...) which already use ArrayBuffer-based File construction. No production code is affected: real browsers handle nested-Blob File construction correctly.
2026-05-17 16:31:19 +02:00
Huang Xin f4483643f4 fix(tts): skip hidden footnotes in TTS, closes #4135 (#4193)
Footnotes/endnotes are hidden in the rendered page via `display: none`,
but TTS builds its blocks from its own document. For background
sections that document is raw XHTML loaded via `section.createDocument()`
without the page layout styles, so the footnotes were read aloud.

- `createRejectFilter` gains an `attributeTokens` option to match
  `aside[epub:type~="footnote|endnote|note|rearnote"]` (value-token
  match, like CSS `[attr~="x"]`), so footnotes are detectable on raw
  documents that lack the `epubtype-footnote` class.
- `TTSController` adds the footnote selectors to its reject filter.
- `getBlocks()` (foliate-js) skips the subtree of any block-level
  element the node filter rejects, ending the preceding block before
  it so footnote text doesn't leak in.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:23:54 +02:00
Huang Xin 9b0072173c feat(metadata): parse Calibre series info from PDF and CBZ (#4066)
Surface series name and index from Calibre-written metadata so the
existing belongsTo.series → metadata.seriesIndex pipeline picks them up.

- PDF: read calibre:series + calibreSI:series_index from XMP
- CBZ: read ComicInfo.xml (Series/Number/Count); fall back to
  ComicBookInfo/1.0 (series/issue) in the ZIP comment

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:32:39 +02:00
Huang Xin d609de58f0 fix(reader): preserve position when toggling scrolled mode, closes #3987 (#3996)
The paginator's scrolled-mode scroll handler is debounced 250 ms, so
#anchor and #primaryIndex can lag behind the user's actual viewport.
Toggling out of scrolled mode within that window made
render() → scrollToAnchor(#anchor) restore the stale anchor, reverting
the position to a previously visible section.

Update foliate-js to flush the pending scroll state before flow leaves
'scrolled', and add regression coverage for the multi-section toggle path.
2026-04-29 09:32:54 +02:00
Huang Xin 234ecc3113 fix(epub): fall back to case-insensitive zip lookups (#3991)
Try an exact ZIP entry match first, then fall back to a case-insensitive lookup for EPUB resources when archive entry casing differs from manifest paths.

Keep ambiguous case-only duplicates exact-match only, and add a regression test that opens a real EPUB through DocumentLoader.
2026-04-28 21:10:10 +02:00
Huang Xin dab92c8a46 fix(pdf): prevent continuous scroll kickback (#3990)
Update foliate-js to preserve the current intra-page scroll anchor when fixed-layout pages relayout or replace placeholders in scrolled mode, instead of snapping back with scrollIntoView().

Add regression coverage for the scroll-anchor restore path.

Closes #3876.
2026-04-28 20:26:55 +02:00
Huang Xin 31e44d2e4d fix(a11y): fixed saving reading progress with screen readers, closes #3864 (#3891) 2026-04-18 14:52:40 +02:00
Huang Xin a5690e9a84 fix(tts): skip br elements in PDF text layer to prevent TTS interruptions at line breaks, closes #3771 (#3811)
PDF.js TextLayer renders <br> between text spans for visual line wrapping.
The TTS SSML generator was converting these to <break> elements, causing
TTS engines to pause at every PDF line break within paragraphs. Fix by
rejecting <br> (along with canvas and annotationLayer) via the node filter
when the document is detected as a PDF.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:34:38 +02:00
Huang Xin ed7cfc31fc fix(layout): fix off-by-one page count on fractional DPR devices, closes #3787 (#3813)
The `pages` getter used Math.ceil(viewSize / containerSize) which inflates
the count by 1 when floating-point drift makes the ratio slightly above an
integer (e.g. 4.00000001 → 5). Use Math.round to absorb sub-pixel drift,
matching the approach already used in #getPagesBeforeView.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:08:55 +02:00
Huang Xin 52ac74bbad fix: fixed status info layout in vertical mode, fixed Android build (#3735) 2026-04-03 21:41:49 +02:00
Huang Xin e68dedd10d fix(layout): fix primary view detection on fractional DPR devices, closes #3681 (#3701) 2026-03-31 15:04:02 +02:00
Huang Xin 797fe9c604 fix(layout): fixed infinite expand calls and freeze in the paginator, closes #3683 (#3690) 2026-03-30 14:50:59 +02:00
Huang Xin 1af55e24da feat(epub): support continuous scroll and spread layout for EPUBs, closes #3419 and closes #1745 (#3546) 2026-03-16 18:24:07 +01:00
Huang Xin 8850e6c00f feat(pdf): support TTS and annotation on PDFs, closes #2149 & #3462 (#3493)
* chore: bump jsdom to the latest version

* feat(pdf): support TTS and annotation on PDFs, closes #2149 & closes #3462
2026-03-09 10:28:19 +01:00
Huang Xin 35ade2c855 fix(tts): handle documents without lang attribute or XHTML namespace, closes #3291 (#3456) 2026-03-03 10:35:33 +01:00
Huang Xin c533da498d feat(annotator): add page number for annotations, closes #3082 (#3405)
* feat(annotator): add page number in annotation

* feat: add page number for annotations, closes #3082
2026-02-28 08:08:56 +01:00