forked from akai/readest
f805477091babdfd6783671dfbe7aea7a47e0980
304 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a837cb50d |
fix(reader): fix PDF text selection misplaced by OS font scaling (#49) (#4960)
Bumps foliate-js to include readest/foliate-js#49, which corrects PDF text selection and highlighting drifting down and spilling into the margins when the device's system font-size accessibility setting is larger than default. Android scales the transparent text layer's glyphs but not the page canvas, so the text layer now divides that scale back out of the glyph size. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
75f1fafe9f |
feat(reader): slide and page curl turn animations (#555) (#4940)
* feat(reader): slide and page curl turn animations (#555) Add an Animation Style setting (Push, Slide, Page Curl) next to the Paging Animation switch. Slide moves the turning page over the still previous or next page like the Apple Books slide; Page Curl folds it open in 3D so the page underneath is partially visible as it turns. Both styles track the finger: the page follows a horizontal drag and commits past halfway or on a flick, or settles back. The page header and footer stay in place while the page turns. The styles layer a View Transitions snapshot of the outgoing page over the live, stationary incoming page, since the pages of one section live in a single iframe and can never be on screen twice. They work for all writing modes including vertical-rl, and on engines without the View Transitions API (older WebViews) the paginator falls back to the existing push animation, so all platforms keep working page turns. The paginator changes live in the foliate-js submodule; this bumps the pointer, wires viewSettings.pageTurnStyle to the renderer turn-style attribute, and adds browser tests covering slide layering, curl, vertical-rl, finger tracking with commit and revert, and the push fallback. Fixes #555 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reader): add WebGL page curl renderer for mesh turn animations (#555) Grid mesh deformed around a cylinder: content past the fold wraps over and lands mirrored on top with a whitened page back, transparent where the page has curled away. Corner grabs start as a steep diagonal pinch that straightens as the turn completes so the whole page clears by the end. Groundwork for the Tauri mesh curl; capture and orchestration land separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(native-bridge): capture webview region as PNG on macOS and iOS (#555) New capture_webview_region plugin command returns a binary PNG snapshot of the calling webview (tauri::ipc::Response, no JSON overhead) for the mesh page-curl texture. macOS goes through WKWebView takeSnapshotWithConfiguration via with_webview on the main thread with a 500ms timeout; iOS snapshots in Swift and hands the PNG across the JSON-only plugin boundary base64-encoded, decoded back to bytes in mobile.rs. Windows, Linux, and Android reject for now so the JS side falls back to the CSS curl. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reader): drive the mesh page curl on Tauri platforms (#555) Wire the WebGL curl renderer and the native webview capture into page turns. A MeshCurlTurn controller runs the pipeline per turn: snapshot the content box, overlay the captured page drawn flat, turn the live view instantly underneath (the paginator's animated paths all gate on the animated attribute), then curl the capture away. Backward turns mirror the fold to the spine edge, matching the layered VT curl's old-page-recedes choreography. useMeshPageCurl wraps the view's prev/next so taps, keys, and wheel turns all curl, and registers a touch interceptor (between the reading ruler and the fixed-layout swipe) that scrubs the curl from the finger, committing past halfway or on a flick and otherwise un-curling and turning back under the overlay. The paginator stays out of the way via no-swipe while the mesh is active; if the native capture ever fails the session falls back to the paginator's CSS arc-fold curl and the shared applyPageTurnAttributes helper restores turn-style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(build): restore iOS builds on Xcode 26.2 with a vendored swift-rs Swift 6.2's driver no longer honors swift-rs 1.0.7's cross-compilation style (swift build --arch <host> with per-swiftc -target overrides and an inherited SDKROOT): plugin sources compile against the wrong platform's Swift overlays and fail with baffling errors like type 'Bundle' has no member 'main' and extra argument 'privacy' in call. Upstream swift-rs is unmaintained, so vendor it under packages/swift-rs via a crates-io patch and build with SPM's first-class --triple/--sdk flags instead, dropping the leaked SDKROOT so the host-targeted manifest compile stays clean. Artifacts land in the unversioned-triple directory now, so the link search path follows. With --triple, SPM enforces the deployment floor declared in Package.swift (the old override bypassed it): bump native-bridge to iOS 15.0, matching the app's deployment target, since StoreKit's Storefront is used unguarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(native-bridge): capture webview region on Android via PixelCopy (#555) Implements the Android side of capture_webview_region so the mesh page curl works there too. The Kotlin command scales the CSS-pixel rect by the display density, offsets it by the webview's window position, and reads the pixels back from the window surface with PixelCopy (API 26+, the app's minSdk), which includes the hardware-accelerated WebView that View.draw would miss. PNG encoding runs off the main thread and the result crosses the JSON plugin boundary base64-encoded, decoded back to bytes in mobile.rs like iOS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): right the upside-down page curl on iOS (#555) The renderer oriented its texture with UNPACK_FLIP_Y_WEBGL, which WebKit ignores for ImageBitmap uploads: on iOS the captured page rendered upside down, and the mirrored page back read as rotated 180 degrees instead of the ink-through-paper horizontal mirror Apple Books shows. Upload unflipped and sample page coordinates directly so no pixel-store flag is involved. The page texture in the browser test was only horizontally asymmetric, which is how the flip slipped through; it now uses four quadrants fed through the production PNG-blob-to-ImageBitmap path and pins the vertical orientation. Verified red/green by running the suite on Playwright WebKit, which reproduces the iOS behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): curl the whole page including header, footer, and margins (#555) The mesh curl captured only the margin-inset content box, leaving the running header, footer, and page margins static while just the text column turned. A physical page turn takes the whole sheet with it, as Apple Books does, so the capture and overlay now span the full reader cell. The overlay mounts above the in-cell header (z-10) and footer, so the static copies never show through the turning page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): gate layered View Transition turns and slide from a capture instead (#555) iOS 18 WebKit ships document.startViewTransition but crashes the WebContent process when a page-turn transition snapshots the reader, so the mere presence of the API is not enough for the layered slide/curl turns. Require nested view-transition groups (Chrome/WebView 140+) as the marker of a mature engine before setting turn-style on the renderer. Engines that fail the check no longer lose the slide on Tauri: the mesh curl's capture pipeline generalizes to CapturedPageTurn and now also drives a flat slide overlay (capture the outgoing page, turn instantly underneath, translate the captured page out toward the spine, mirrored for backward turns), clipped to the content box with an edge shadow like the VT slide. On the web, engines without full support fall back to push and the Slide/Page Curl options are hidden from the Animation Style select; a synced slide/curl setting from another device reads as Push there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): make the Android page curl start instantly (#555) The Android capture encoded a full-density PNG: 1080x2400 on a 3x Xiaomi 13 took ~1.5s per turn, so the page sat frozen long enough to read as the curl not working at all. Encode JPEG instead (the page is opaque) and cap the destination bitmap at 2x CSS pixels - PixelCopy scales into a smaller bitmap for free and the moving page stays sharp. Measured on device over CDP: the capture invoke drops from 1550ms to 34ms and the curl overlay mounts 132ms after the tap. The JS side stops hardcoding an image/png blob type and lets the decoder sniff the platform's actual format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(reader): encode iOS page-curl captures as capped JPEG (#555) Apply the Android speedup to iOS: encode the snapshot as JPEG (the page is opaque) off the main thread, and cap it at 2x CSS pixels via WKSnapshotConfiguration.snapshotWidth on 3x screens, cutting both the encode time and the base64 payload crossing the JSON plugin boundary. The JS side already sniffs the image format from the bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec45a080fc |
feat(metadata): surface calibre custom columns from EPUB metadata (#4939)
Parse calibre's embedded user metadata (custom columns) from the OPF in foliate-js, store it on BookMetadata.calibreColumns, render the columns in the book details view, and match column names and values in the library search so a value like a recommends tag can be found by typing it. Closes #4811 |
||
|
|
42f9b8fe3c |
feat(tts): gapless Web Audio playback engine for Edge TTS with chapter timeline and seek (#4931)
* feat(tts): add PCM speech-bounds detection for sentence audio trimming Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): add WSOLA time-stretch for pitch-preserved playback rate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): add sentence duration store with per-voice speaking-rate calibration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tts): serve edge audio as ArrayBuffer with in-flight fetch dedup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): add WebAudioPlayer with gapless chunk scheduling and backpressure Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): play edge TTS through gapless Web Audio pipeline Replaces the per-sentence audio element with trimmed, time-stretched buffers scheduled on the shared AudioContext. Marks dispatch at audible time so schedule-ahead cannot run foliate's cursor past the voice; a decode failure or missing audio skips the chunk instead of wedging the session; pause and resume ride context suspend and resume with no iOS rewind hack; the object-URL cache is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): add section timeline with measured and estimated sentence durations Includes the foliate-js submodule bump for the getSentences export (fork branch feat/tts-get-sentences; fork PR must merge before this lands so the pinned SHA resolves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): expose section playback position and sentence-snapped seeking Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): surface playback position and seek in the media session Position state is clamped, never skipped, so the lock-screen scrubber stays live when estimates overshoot; seekto units map per backend (native ms, web seconds). The AudioContext warms up in the tts-speak gesture path before any await, and the silent keep-alive element now runs on all platforms so desktop hardware media keys survive the removal of the per-sentence audio element. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): add seekable chapter progress bar to the TTS panel The scrubber joins the transport cluster with a thin range-xs track and flanking tabular time labels so it cannot be misgrabbed for the chunky rate slider (which persists a global setting). States: reserved disabled slot until the lazy timeline lands, persists across chapter transitions, optimistic thumb with failure toast, monotonic position, tilde-prefixed estimated totals, sentence-event updates under e-ink. The popup grows only when a timeline-capable client is active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record deferred TTS listening-engine follow-ups Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record background TTS decoupling design decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): slim the panel scrubber to a native track with remaining time Match the footer Jump to Location slider (plain native range: thin track, small thumb) instead of the chunky daisyUI pill, show remaining time with a minus prefix on the right, and drop the This chapter caption. Popup height shrinks accordingly. Verified live in Chrome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: pin foliate-js to merged main with getSentences export Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tts): catch autoplay rejection from the keep-alive element Running the silent keep-alive on all platforms exposed an un-awaited play() that headless Chromium rejects without a user gesture, failing CI on unhandled rejections while every test passed. The keep-alive is best-effort; the production path is gesture-qualified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
cd3a53f507 |
fix(sync): WebDAV Sync now pulls latest book metadata and merges config (#4756) (#4776)
* fix(sync): pull newer WebDAV book metadata to devices that already hold the book (#4756) syncLibrary only pulled title/author/cover for books missing from the local library. For a book a device already held it only pushed, so a peer's metadata edit never propagated back, and the final library.json re-push clobbered the peer's newer metadata with this device's stale copy. Add a last-writer-wins reconciliation pass keyed on book.updatedAt: when the shared index has a strictly newer copy of a locally-held book, merge its metadata, re-pull the cover, persist it via a new updateBookMetadata callback, and keep the merged copy authoritative for the index re-push so neither direction loses the edit. Surface a "metadata updated" counter in the sync toast and history, and translate the new strings across all locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): merge remote config before pushing in WebDAV Sync now (#4756) The manual library "Sync now" pushed each book's config.json blind, so it could overwrite a peer's booknotes (element-set CRDT) or regress newer remote progress (per-config LWW) that this device had not pulled yet. The reader hook already pull-merges before pushing; the library path did not, so notes and progress could diverge or regress on the remote until a device happened to open the book. Give syncLibrary's config push the same read-merge-write cycle: pull-merge then push the merged superset, persisting it locally so the device converges too. Gated on canPull so 'silent' converges while 'send' keeps the local copy authoritative and 'receive' still never pushes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0c7ffa9799 |
fix(reader): stop iOS page-turn animation stutter (#4768) (#4772)
* fix(reader): stop iOS page-turn animation stutter (#4768) iOS users saw occasional page-turn animation stutter that was not present on earlier 0.11.x builds. It traces to foliate-js commit c1c7315 (first shipped in 0.11.4): the large-section rafAnimateScroll fallback and the removal of persistent compositor-layer hints, both added to fix a ~1s Blink freeze on Android Chromium at high DPR. Apple WebKit composites those layers fine, so on iOS (notably 120Hz ProMotion devices) the changes only cost smoothness: large-section turns animate scroll on the main thread, and every turn promotes a layer on-demand instead of using a persistent one. Opt the iOS renderer into foliate-js's new gpu-composite path, which restores persistent compositor layers and skips the main-thread rafAnimateScroll fallback. Other platforms keep the Android freeze fix. Bumps the foliate-js submodule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): repin foliate-js to merged gpu-composite commit (#4768) readest/foliate-js#39 squash-merged to a new commit on main. Move the submodule pin off the now-orphaned PR branch commit to the merged main commit. No content change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
163487b5e3 |
feat(reader): add regex and nearby-words search modes (#4560) (#4764)
Add Calibre-parity search modes to the reader's full-text search. The "Match Whole Words" toggle becomes a single-select mode group: Contains, Whole Words, Regular Expression, Nearby Words. - Regex and nearby-words matching live in the foliate-js submodule (bumped here); the sidebar threads `mode` and `nearbyWords` through. - Nearby distance is chosen with a "within N words" control (5/10/20/50, default 10), not parsed from the query, so trailing numbers stay literal search words. - Per-mode modifiers: Match Diacritics is greyed out for regex (no-op). - Calm inline error for invalid regex / too-few nearby words, a no-results state, and a results-count footer. - Nearby matches render a segmented excerpt emphasizing each matched word and highlight every word in the book. - BookConfig schema v2 -> v3 migrates the deprecated `matchWholeWords` boolean to `mode` (still written for sync back-compat). Also fix two search interactions: - option changes (e.g. within-N-words) now take effect immediately by reading the latest config at search time instead of a stale closure. - closing search from the results nav bar now exits the sidebar search mode, not just the results (search-bar visibility lifted to the store). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6e9faaa874 |
fix(pdf): throttle PDF range reads to fix large-file OOM on Android/iOS (#3470) (#4670)
Large PDFs (50 MB+) crashed on import/open. pdf.js requests hundreds of byte ranges in a burst while parsing the document structure, and foliate-js makePDF dispatched them all concurrently. On Android each read is served through the WebView's rangefile custom scheme (shouldInterceptRequest); the flood of simultaneous native requests exhausts the WebView's Java heap (OutOfMemoryError in handleRequest). Bump foliate-js to cap in-flight range reads at 6 (the implicit per-host limit a real HTTP transport already gets), and add a regression test asserting makePDF keeps at most 6 range reads in flight. Verified live on a Xiaomi 13 (Android 16 / WebView 147) via CDP: max concurrent range reads drop from 753 to 6 with no change in open time. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
403be32d5a |
fix(epub): import books whose OPF has an unescaped ampersand (#4640)
Some EPUBs ship an OPF that isn't well-formed XML — a bare, unescaped `&` in a hand-built manifest id, e.g.: <item id="Chapter_1213_Search_&_Rescue_153" .../> A strict XML parser rejects it (`EntityRef: expecting ';'`) and the book fails to import on every platform: the web/desktop/Android reader-open path (foliate `EPUB.#loadXML`) and the Android/desktop native-import bridge (`parseEpubMetadataFromXML`, which parsed unsanitized). Bump foliate-js to escape any `&` that doesn't begin a valid character or entity reference, applied at both parse sites (readest/foliate-js#26). Valid and numeric references are preserved. Verified end-to-end on the real "Shadow Slave - Vol. 6" EPUB: imports in the web app (Chrome) and on a physical Xiaomi device via the native path, both of which previously failed. New unit test covers both `parseEpubMetadataFromXML` cases. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5e5564ef3f |
fix(search): show context for matches in italicized text (#4594) (#4631)
Fulltext search showed only the matched word with no surrounding context when the match fell inside inline-styled text (e.g. <i>/<em>). The root cause and fix live in the foliate-js submodule's makeExcerpt (readest/foliate-js#25); bump the submodule pointer to pick it up and add a regression test covering both the simpleSearch and segmenterSearch paths. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a30a310a17 |
fix(opds): handle entries with no downloadable format (#4599) (#4611)
An OPDS entry with full metadata and a cover image but no acquisition link — e.g. a Calibre book whose file was removed but kept for tracking borrowed/loaned titles — was classified by foliate-js as a navigation item whose href fell back to the cover image link. Tapping it loaded the image, which is neither XML nor JSON, so the OPDS browser crashed with a JSON parse error. - Bump foliate-js to include the getFeed fix that classifies such metadata-only entries as publications instead of navigation. - PublicationView: show "No downloadable format available" when an entry has no acquisition or stream links. - loadOPDS: defense-in-depth — surface a clear message instead of a raw JSON.parse SyntaxError when a response is neither XML nor JSON. - Add tests covering the Calibre no-format entry and a regression guard that a true navigation entry still classifies as navigation; add the two new UI strings across all locales. Closes #4599 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
64350ca632 |
fix(reader): keep scrolled-mode scrollbar visible after opening a book (#4470) (#4538)
Bump foliate-js to include readest/foliate-js#22. The scrolled-mode scroll container (#container) lost its compositing layer in the GPU-hint cleanup, so on Windows' always-on scrollbars the scrollbar appeared on open then vanished once adjacent-section preloading changed the content height. Restoring transform: translateZ(0) on the scrolled #container keeps the scrollbar composited so it repaints across content-size changes. Closes #4470 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
31176e5d47 |
fix(paginator): bump foliate-js submodule for scrollBounds guard (#4526)
Pulls in the foliate-js fix that guards Paginator#scrollBy and Paginator#snap against an uninitialized #scrollBounds. Without the guard, a swipe that lands before the first #scrollToPage seeds the bounds (e.g. a fast swipe right after the reader mounts, or while a section is still loading) crashes with TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator)) at Paginator.snap The submodule fix bails out of both entry points when the bounds aren't ready yet, so the swipe is dropped rather than fatal; subsequent settled scrolls reseed the bounds and swipe handling resumes. |
||
|
|
11d796361e |
perf(import+open): native Rust EPUB/MOBI parser, OPF prefetch, parallel TOC enrichment (#4369)
* perf(epub): add native EPUB parser in Rust
Introduce a Rust-side EPUB pre-parser exposing three Tauri commands:
* parse_epub_metadata - title/author/cover + partialMD5 in one
shot, for the import hot path
* parse_epub_full - OPF + nav.xhtml + toc.ncx bytes plus a
manifest size table, for the reader open
hot path
* extract_epub_cover_full - full-resolution cover bytes, for the
lock-screen wallpaper writer
All three avoid ferrying multi-MB blobs across the JS<->Rust IPC
boundary. Cover bytes returned by parse_epub_metadata are downscaled
to a webview-friendly JPEG when the long edge exceeds the library
thumbnail size.
No JS callers yet -- wired up in the following commits.
* perf(import): use native EPUB parser and downscale covers on Tauri targets
On Tauri (desktop/iOS/Android), importBook now forwards EPUB
metadata + cover extraction to the Rust parse_epub_metadata
command and reuses the partialMD5 it returns, skipping the
foliate-js full archive parse and the second pass over the file
for hashing.
As a side effect, the cover written to cover.png is downscaled
to a webview-friendly JPEG (long edge <= 512px), shrinking the
on-disk thumbnail from multi-MB to ~30-60KB per book. To keep
the lock-screen wallpaper feature unchanged, useAutoSaveBookCover
now pulls the original full-resolution cover via the Rust
extract_epub_cover_full command instead of copying the (now
downscaled) cover.png; falls back to the thumbnail when the
native path is unavailable.
Web targets and non-EPUB formats keep the existing path.
* perf(reader): prefetch EPUB OPF/nav from Rust on book open
When opening an EPUB on Tauri targets, DocumentLoader now calls the
Rust parse_epub_full command up-front to pull the OPF, EPUB3 nav,
NCX and the central-directory size map in a single IPC. The
foliate-js zip loader is wrapped so that loadText() of these
entries (and a synthetic META-INF/container.xml) is served from
that in-memory cache without inflating through zip.js, while
all other assets keep flowing through the original loader.
A small in-flight dedupe is added to the spine-text loader so the
nav pipeline (loadText + createDocument back-to-back on the same
href) doesn't pay for two zip.js inflate calls per chapter on
first open.
Reader store / app service plumbing: readerStore.openBook now
resolves an absolute on-disk path via the new
appService.resolveNativeBookFilePath / bookService.resolveNativeBookFilePath
helper and threads it into DocumentLoader as nativeFilePath so
the prefetch can fire. Web targets, non-EPUB formats and books
without a managed/external on-disk path skip the prefetch and
take the original code path.
* perf(nav): parallelize section scans and memoize fragment lookups
computeBookNav now processes sections via Promise.all instead of
a sequential for-loop, and within each section issues loadText()
and createDocument() concurrently. Combined with the in-flight
loadText dedupe added to the zip loader, each chapter pays for a
single zip inflate per nav build, and the inflates of different
chapters overlap.
enrichTocFromNavElements is restructured into two concurrent
phases: a cheap '<nav' substring filter on the inflated text, and
a parsed-document walk for the survivors. Most chapters fall out
in phase 1 without ever being parsed.
In fragments.ts, calculateFragmentSize now consults a
per-section position cache (makeFragmentPositionCache) so the
N-fragment loop is O(N) over the chapter HTML instead of O(N²).
A small isCfiAddressable guard is added to skip elements that
foliate-js's CFI generator can't address (documentElement, body
itself, detached nodes, nodes outside <body>) — these previously
threw and spammed console.warn for every fragment, now they
silently fall back to the section CFI.
* perf(import): use native MOBI/AZW/AZW3 parser on Tauri targets
On Tauri (desktop/iOS/Android), importBook now forwards
MOBI/AZW/AZW3/PRC metadata + cover extraction to the Rust
parse_mobi_metadata command and reuses the partialMD5 it returns,
skipping the foliate-js full-buffer parse and the second pass over
the file for hashing. Mirrors the existing EPUB native fast-path
added in e3fc4767 — bookService tries EPUB first, then MOBI; both
bridges fall back to the foliate-js DocumentLoader when the native
path is unavailable (web target, parse error, format mismatch).
The new mobi_parser is built on the mobi crate (KF7+KF8 reader,
zero JS-side touch). It reads title, author, publisher, ISBN, ASIN,
publish date, language, subjects and description from the MobiHeader
+ EXTH records, resolves the EXTH 201 cover offset against the PDB
image-record table (with ThumbOffset / first-image fallbacks), and
strips KindleGen's HTML wrapping in EXTH 103 so the description goes
into the library DB as plain text. The parsed cover is funneled
through the same maybe_resize_cover path as EPUB, so MOBI library
thumbnails are also clamped to a 512px-long-edge JPEG.
Cover-resize / partialMD5 / RawCoverImage are extracted into a new
parser_common module shared between epub_parser and mobi_parser, so
a single tweak (e.g. raising the thumbnail target) applies to every
native importer and the partialMD5 implementation can't drift between
the two paths (a divergent algorithm would silently re-import every
existing book under a new hash on the first run).
Web targets and non-Kindle formats keep the existing path.
* test(tauri): verify native Rust EPUB parser parity with foliate-js
Add a Tauri WebView parity suite (epub-parser-parity.tauri.test.ts) that
cross-checks the native Rust parser against foliate-js on the same fixtures:
parse_epub_metadata / parse_epub_full (title, author, language, identifier,
publisher, published, subjects, partialMD5, OPF + per-entry size table), and
that opening with the native prefetch produces the same BookDoc and
computeBookNav (TOC) output as the pure-JS path.
Fix a parity divergence the suite caught: the Rust OPF parser mapped
dcterms:modified onto `published`, but foliate-js keeps them separate and
leaves `published` empty -- so EPUB3 books carrying only the mandatory
dcterms:modified got a bogus publication date on the native import path. Map
only dc:date now; add regression tests.
Test infra:
- vitest.tauri.config.mts: add optimizeDeps (mirroring vitest.browser.config)
so foliate-js-importing tauri tests load -- otherwise esbuild's dep scan
can't resolve '@pdfjs/pdf.min.mjs', pre-bundling is skipped, and the CJS
deps fail to import ("Importing a module script failed").
- capabilities-extra/webdriver.json: fix __test__ -> __tests__ fs scope typo
so import tests can open fixtures under src/__tests__/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(import): foliate-js owns EPUB/MOBI metadata via standalone extractors
Rust contributes only the mechanical work that's expensive on a
WebView — partialMD5, the downscaled cover, and (for EPUB) the raw
OPF bytes Rust already had to read for cover resolution. Metadata
extraction is delegated to foliate-js's two new standalone entry
points (`parseEpubMetadataFromXML`, `readMobiMetadata`) so the
import-path BookDoc and the reader-path BookDoc share a single
parser implementation.
EPUB
- `parse_epub_metadata` returns
`{ partialMd5, cover, coverMime, opfPath, opfBytes }`. OPF bytes
are a free byproduct of the cover-resolution scan.
- `tryNativeParseEpub` runs `parseEpubMetadataFromXML` on the OPF
bytes and assembles a lightweight BookDoc stub (metadata +
getCover). The importer doesn't drive `DocumentLoader.open()`, so
no zip central-directory scan, no nav/ncx inflate, no spine walk.
- `coverMime` is preserved so `bookService.importBook`'s
`cover.type === 'image/svg+xml'` branch still routes SVG covers
through svg2png.
MOBI / AZW / AZW3 / PRC
- `parse_mobi_metadata` returns `{ partialMd5, cover, coverMime }`.
`tryNativeParseMobi` runs foliate's `readMobiMetadata` on the
same File, which uses `MOBI.open(file, { metadataOnly: true })`
to parse PalmDB + MobiHeader + EXTH and short-circuit before the
MOBI6 / KF8 init() that walks every text record.
- `Book.metadata.identifier` is foliate's `mobi.uid.toString()`
(PalmDB UID), the canonical MOBI identifier the reader path uses.
bookService.importBook
- EPUB and MOBI native branches consume the bridge's BookDoc stub
directly. The stub's `getCover()` returns the Rust-downscaled
blob, falling back to foliate's own `getCover` thunk when Rust
didn't extract a cover.
Other
- Drop the unused `base64` Rust dependency: cover bytes go over IPC
as `Vec<u8>` (Tauri 2 transports them natively, like opfBytes /
navBytes / ncxBytes).
- Drop the `nativePrefetch` option on `DocumentLoaderOptions`; no
caller passes it. `nativeFilePath` keeps driving `parse_epub_full`
on the open hot path.
Tests
- vitest.tauri parity test asserts byte-equal partialMD5, cover
presence parity, OPF bytes that decode to a real `<package>`
document, and that `parseEpubMetadataFromXML` on those bytes
produces the same user-visible metadata fields (title / author /
language / identifier / published) as `DocumentLoader.open()`.
* test(tauri): add War and Peace MOBI fixture for native parser parity
The .tauri parser-parity suite previously had no .mobi/.azw3 asset, so the native MOBI parser (metadata + EXTH cover resolution) was uncovered. Adds a real KF8 MOBI ("War and Peace") to enable MOBI parity coverage against foliate-js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(foliate-js): bump submodule to readest/foliate-js main (91191ca)
Replaces the ad-hoc 02f435a with the merged main commit 91191ca, which lands the standalone OPF/MOBI metadata extractors (parseEpubMetadataFromXML, readMobiMetadata) the import fast-path depends on (foliate#19), plus the RTL multi-view rect-mapper fix (foliate#20). The extractor code is byte-identical to 02f435a, so the bridges are unaffected.
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>
|
||
|
|
553c2b6398 |
fix(linux): update tauri submodule for resize cursor fix (#4512)
Update tauri submodule to include input_shape fix that lets edge pointer events fall through to the GtkWindow for native resize cursor on Linux. |
||
|
|
676e14234b |
fix(reader): correct RTL reading position restore on book reopen (#4505)
Bumps foliate-js to pick up the RTL multi-view rect mapper fix (readest/foliate-js#20). Reproduced with an Arabic EPUB: closing the book at e.g. chapter 2 page 14 and reopening it briefly landed near the saved position, then jumped several chapters forward as foliate-js's #fillVisibleArea pre-loaded adjacent sections; the wrong location was then auto-saved, overwriting the user's actual reading progress on disk. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
f8d88cca50 | chore: bump tauri plugins (#4373) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
648c35b334 |
feat(reader): add disableSwipe option to disable swipe-to-paginate (#4335)
Issue #4288: users with hardware page-turn buttons (e.g. e-ink readers) want to disable swipe-to-paginate so accidental finger drags during highlight selection don't flip the page mid-annotation. The existing "Tap to Paginate" toggle only covers taps; swipe was always on. - New `disableSwipe: boolean` on `BookLayout` (default `false`, declared right after `disableClick`). - foliate-js submodule bump: paginator gates `#onTouchMove` and `#onTouchEnd`'s snap-to-page on a new `no-swipe` attribute, so native touch behaviour (text selection) stays intact. - `FoliateViewer` sets/removes the `no-swipe` attribute alongside `animated`, and the `ControlPanel` toggle pushes the change to the live renderer so it takes effect without a viewer reset. - The fixed-layout swipe interceptor in `usePagination` also bails when `disableSwipe` is on, covering both reflowable and fixed- layout books. - New "Swipe to Paginate" UI row directly below "Tap to Paginate"; both can be off simultaneously. - i18n: 33 locales translated. Also polish: rephrase the two helper texts under "Read books in place" in `ImportFromFolderDialog`. The previous copy ("Copy no book into the library to save space.") used an awkward double-negative; the new wording is clearer and the locked variant drops "registered as" / em-dash for a plain two-sentence form. i18n updated. Closes #4288 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
93abca8960 |
feat(dict): faster MDict/StarDict import + lazy lookup; raw .dict; UX (#4334)
Make the dictionary import path usable on large bundles and bring the multi-device flow up to par. Import perf - Skip `MDX.create()` at import time. The factory triggers full init — decompresses every key block and sorts millions of keys with localeCompare just to expose the header. Replace with a tiny `readMdxHeader()` that only reads the small XML header for Title / Encoding / Encrypted (saves ~17 s on a 250 MB MDX on web). - `partialMD5`: read the 9 sample slices in parallel rather than sequentially. Each freshly-picked-File slice round-trip on Chrome costs ~100 ms cold; parallelisation collapses 9 of them into one. - Native fast-path in `nativeAppService.writeFile`: when the source is a `NativeFile`, delegate to Tauri's `copyFile` rather than streaming the file through `NativeFile.stream()`. Streaming a 250 MB body through 1 MB IPC chunks on Android took ~100 s; native copy is bound by disk throughput instead. Exposes `NativeFile.getNativeLocation()` for the FS layer to use the underlying path + baseDir directly. Lazy lookup - Pass `lazy: true` to `MDX.create` / `MDD.create`. The js-mdict change in this PR skips the upfront decompress-every-block + sort during init (~80 s on the same 250 MB bundle) and decodes only the relevant key block on demand per lookup. First-lookup main-thread block drops from ~81 s to ~230 ms. (Closes #4228.) Raw .dict - Drop the import-time gate that flagged non-gzip dict bodies as `unsupported`. The runtime body loader (`loadDictBody`) already probes the gzip header and falls through to a passthrough buffer for raw files, so the gate was the only thing preventing raw `.dict` bundles from importing on devices that received them via cloud sync. (Closes #4179, partially addresses #4248.) Import-flow UX - `handleImport` now always surfaces a toast for every non-cancelled attempt: picker errors, missing app service, no-op imports, and unsupported-but-imported bundles each get their own message instead of failing silently. - Call `markAvailableByContentId(newDict.contentId)` after add/replace so the "Bundle is missing on this device" warning clears immediately — no need to close-and-reopen the panel. System Dictionary - Drop the cascading toggle behavior in `setEnabled`. Each provider's enabled flag persists independently; exclusivity is enforced at lookup time. Toggling System on/off no longer wipes the user's preferred set of in-app providers. - Render non-system rows as read-only when System is on (toggle still shows what's queued to restore; tooltip explains the lock). - `isSystemDictionaryEnabled` short-circuits to `false` on platforms where the handoff isn't implemented. `providerEnabled` is whole- field synced across devices, so a flag set on macOS would otherwise leak to a Windows device with no way to look up a word. js-mdict - Submodule bump to e6dbc99 which adds the opt-in `lazy: true` `MDictOptions` flag (skip `_readKeyBlocks` + post-init sort; new `lookupKeyBlockByWordLazy` path on `MDX` and `MDD`). Eager mode is unchanged and every existing js-mdict test still passes. i18n - 208 new translations across 33 locales for the new UX strings. Closes #4228 Closes #4248 Closes #4179 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cf44e85180 |
fix(reader): fit duokan-page-fullscreen cover image without cropping (#4328)
Closes #3914. Switch the page-fullscreen image to object-fit: contain (and SVG preserveAspectRatio meet) so cover images fit the page and center without cropping. Also adds a duokan-image-gallery-cell layout helper and drops the mobile marginBottomPx override. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
1d4b7eed87 |
fix(txt): merge scene-break sections into the preceding chapter (#4063) (#4207)
The TXT-to-EPUB segment regex splits on dash dividers (`-{8,}`), which
authors commonly use as in-chapter scene breaks. Each heading-less section
after such a divider was emitted as its own chapter — a numbered paragraph
fallback chapter, or a chapter titled after a stray sentence — flooding the
generated TOC with entries that aren't real chapters.
Mark chapters with whether their title came from a detected heading, and
merge heading-less chapters into the preceding detected chapter instead of
pushing them as separate TOC entries. Fully heading-less text still chunks
into numbered fallback chapters as before.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a20f68fc11 |
feat(readwise): allow overriding the Readwise sync base URL (#4196)
* feat(readwise): allow overriding the Readwise sync base URL Add an advanced option to point Readwise sync/export at a custom, Readwise-compatible endpoint instead of the hardcoded official API. When the override is unset or blank, behavior is unchanged. - ReadwiseClient resolves a custom `baseUrl` over `READWISE_API_BASE_URL`, trimming whitespace and trailing slashes. - ReadwiseSettings gains an optional `baseUrl` field; it syncs as plaintext via the settings sync whitelist. - ReadwiseForm exposes the URL under a collapsed "Advanced" disclosure on the connect screen, and surfaces a custom URL read-only once connected. Disconnect preserves the custom URL for easy reconnect. Closes #4114 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n(readwise): rename "Sync Base URL" label to "Custom URL" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |