db1d63cdcc7f3a666abcfae60343bc27fa659244
2325 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
db1d63cdcc |
test(reader): harden fixed-layout wheel double-scroll test against CI flake (#4978)
The readest#4727 regression test set scrollTop=0, dispatched a synthetic wheel, waited 60ms, then asserted scrollTop stayed 0. On slow CI runners it flaked with "expected 4 to be +0". As sibling scroll pages finish loading, the renderer runs restoreScrollModeAnchor asynchronously, which at scrollTop=0/page-index-0 snaps scrollTop to page 0's offsetTop, the 4px scroll-page-gap margin. The 60ms post-dispatch delay raced that re-anchoring, so the assertion observed 4 instead of 0. That 4 is unrelated to the wheel bug, which is a 120px jump. The buggy handler was scrollBy with instant behavior, a synchronous scroll that lands before dispatchEvent returns. Measure scrollTop synchronously before and after the dispatch with no await in between and assert they match. This isolates the wheel handler's own effect and is immune to the async re-anchoring. Reintroducing the bug still fails the test (before=4, after=124, a clean 120px delta). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
57868a138e |
feat(settings): unified Cloud Sync chooser with Readest Cloud as a first-class provider (#4976)
The Integrations section is now one Cloud Sync chooser: Readest Cloud,
WebDAV, and Google Drive as radio rows, Readest Cloud first, with a
scope subtitle stating what the choice governs (library data, on this
device) and what always stays with the Readest account (settings,
statistics, dictionaries).
- Activation helpers move to services (cloudSyncActivation.ts) and
accept 'readest' (= no third-party provider active); the component
module re-exports for existing imports.
- Row status lines come from a pure, fully-tested state matrix
(cloudSyncStatus.ts) so every string is enumerated in one place:
signed-out / loading / active / available for Readest Cloud;
not connected / configured / paused / syncing / sync failed /
book-file-uploads-off warning / active for third-party rows. The
paused state renders on the affected third-party row (the plan sketch
placed it on the Readest row; the provider that is paused is the
third-party one).
- The Readest Cloud row opens an inline sub-page (SubPageHeader + the
storage/translation Quota + a NavigationRow out to Account) instead of
ejecting from the Settings dialog; signed-out taps go to login and the
radio is unchecked and disabled, so an idle signed-out state never
shows a checked radio while nothing syncs.
- Premium-gated builds keep the Readest Cloud row and show the upgrade
prompt only in place of the third-party rows.
- Two-direction capability Tips in the WebDAV/Drive sub-pages spell out
both what syncs only to the user's server and what still flows through
the Readest account; the Upload Book Files description notes that
Readest Cloud uploads pause while the provider is selected.
- Manage Sync book/progress/note rows swap their description to
'Managed by {{provider}}...' via the existing locked-row pattern while
a third-party provider is selected; the toggles stay interactive and
persist since they govern the native channels after switch-back.
- The chooser rows form a radiogroup (native same-name radios provide
arrow-key group movement) with an accessible group label.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e08622b416 |
feat(sync): route library sync exclusively to the selected cloud provider (#4380) (#4975)
While WebDAV or Google Drive is the selected cloud sync backend, the native Readest Cloud book/progress/note channels are gated off and the file-sync engine owns library data end to end: - isSyncCategoryEnabled returns false for book/progress/note (and their legacy aliases) when a third-party provider is selected. A runtime override, deliberately not written into syncCategories: the user's own toggles persist and take effect again on switch-back. Account channels (settings, stats, dictionaries, fonts, textures, OPDS catalogs) always stay native. - persistActiveCloudProvider is the single write path for provider switching, used by the chooser, both connect/disconnect flows, and the Drive OAuth callback (which previously bypassed the cross-window broadcast). The broadcast carries ONLY the enabled flags plus providerSelectedAt, never credentials or sync cursors, and only on switch events, so a stale window's routine save cannot revert a switch. - buildWebDAVConnectSettings no longer pre-sets enabled: activation belongs to withActiveCloudProvider, so the fresh-connect path now gets the syncBooks auto-flip and the providerSelectedAt stamp. - Sync health: fileSyncStore records lastError per backend; the durable lastSyncedAt stays in provider settings. The SettingsMenu sync row reads Synced via provider / Sync failed and its tap (with pull to refresh and BackupWindow, all routed through pullLibrary) runs the file engine via the shared runActiveFileLibrarySync helper instead of a gated native pull that would toast undefined book(s) synced. - Mixed-fleet detection: while gated, the auto-sync interval runs a read-only probe of /api/sync since providerSelectedAt; any newer book row means another device still syncs natively, and a once-per-session notice explains the fork instead of leaving it silent. - Readest-Cloud-only affordances hide while a third-party provider is selected: the quota row becomes a caption naming the active provider, Auto Upload disappears from the menu and command palette, the BookItem upload badge and the Transfer Queue Upload All button hide. - providerSelectedAt added to both provider settings types and the backup blacklist. Stacked on the quota-decoupling change for #4959; requires the metadata-parity change so gated channels lose nothing users can see. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f805477091 |
perf(koplugin): defer and cache Library group covers (#4954) (#4974)
Opening the Library with a large grouped library was slow because each folder's 2x2 cover mosaic was recomposed from scratch on every paint (up to 4 MuPDF cover decodes plus scales per cell). On a 685-book library this dominated the synchronous open path (254ms of 300ms) and ran again on the post-sync refresh. Navigation felt fast only because drilling into a group shows single covers, not mosaics. Mirror cloud_covers' async pattern in group_covers: - Cache the composed master bb per group, keyed by a signature that flips when the child set or any child's cover availability changes; serve cheap copies on a hit. Cache a nil result too, so a group whose covers are not ready yet keeps its placeholder without recomposing on every refresh; a later cover download flips the signature and recomposes once. - Compose off the first-paint path: a miss enqueues a background job (one mosaic per UI tick) and returns nil so the cell paints its FakeCover placeholder immediately; finished mosaics coalesce into one refresh. - Free cached masters when the Library closes. Measured synchronous open path drops from 300ms to 151ms on a 685-book library; mosaic compositing moves off the blocking paint and fills in progressively. Adds open-path timing logs to librarywidget and localscanner for on-device diagnosis of future large-library reports. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a72f535346 |
feat(sync): propagate tags and reading status through third-party file sync (#4973)
The library.json index already carries full Book objects, but the metadata merge overlay dropped tags and readingStatus on apply, so tagging or marking a book Finished never reached peers syncing via WebDAV or Google Drive (the same overlay gap that hit group membership in #4942): - mergeBookMetadata carries tags with the metadata LWW subset (raw assignment, so tag removal clears on peers) and merges readingStatus on its own readingStatusUpdatedAt clock, the client-side mirror of the native field-level server merge. This survives the asymmetric race where one device edits metadata after a peer changes the status; whole-book LWW alone would drop the status change. - New shouldApplyRemoteBookMetadata reconciliation predicate triggers on either clock; the engine's index reconcile uses it so a status-only change propagates without a metadata edit. - Merge-law tests (direction, removal, idempotence, asymmetric races) plus engine-level propagation tests for both fields. Prerequisite for gating native sync when a third-party provider is selected (#4380): third-party sync should reach metadata parity before it becomes the only channel. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
942c062d35 |
fix(sync): decouple Readest Cloud storage quota from third-party cloud sync (#4959) (#4971)
When a third-party provider (WebDAV or Google Drive) is the selected cloud sync backend, Readest Cloud storage is no longer written to: - New src/services/sync/cloudSyncProvider.ts policy module: the selected provider is derived from the existing per-device enabled flags (webdav wins deterministically if both are ever set); the premium guard resolves to a PAUSED state instead of silently falling back to Readest Cloud, with the user plan cached for non-React modules. - transferManager gates book uploads on the selected provider: queueUpload returns null when gated; pending book uploads from before a provider switch are visibly cancelled (cancelReason policy) and pruned on the next restore; downloads and replica transfers are never gated. - Book uploads are deferred until settings hydrate (settings.version barrier) so a persisted queue cannot be mis-processed at startup; replica transfers are not stalled. - Quota-exceeded uploads fail fast with zero retries, and a batch import produces one summary toast instead of one toast per book. - Policy cancellations are a distinct bucket via a shared predicate: excluded from failed stats, Retry All, and the per-item Retry button. - Auto-upload call sites (ingest, OPDS, subscriptions) check the provider gate; the explicit Upload Book action explains the gate with a toast instead of silently doing nothing. - Activating a provider auto-enables its syncBooks so books keep backing up somewhere; a one-time migration (20260706) applies the same flip for users who already had a provider enabled. - webdav.deviceId and webdav.lastSyncedAt are excluded from backups, matching the existing googleDrive entries. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a02b236e97 |
fix: more production crashes (View Transition noise, book-dir race, stats transaction) (#4962)
* fix(sentry): drop more benign View Transition rejections Broaden is_ignored_browser_error to also drop "Transition was skipped" (navigation superseded, READEST-F) and "aborted because of invalid state" (READEST-G), matched case-insensitively alongside the existing hidden-tab case. These are expected browser behavior — the navigation completes, only the animation is skipped/aborted. A transition timeout stays visible (real perf signal, handled separately). Fixes READEST-F Fixes READEST-G Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(library): create the book directory idempotently Importing a book did a check-then-create with a non-recursive createDir. Two concurrent imports of the same book both pass the exists check, then the second create fails — on Windows with "Cannot create a file when that file already exists" (Sentry READEST-H). Use a recursive create (create_dir_all), a no-op when the directory already exists. Fixes READEST-H Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(stats): serialize applyRemoteEvents to avoid nested transactions The statistics connection is shared across ReadingStatsTracker instances (split view). applyRemoteEvents runs a manual BEGIN/COMMIT that the per-op native connection lock does not make atomic, so two concurrent pulls opened a BEGIN inside a BEGIN ("cannot start a transaction within a transaction", Sentry READEST-N). Serialize applyRemoteEvents against itself with a small promise mutex. Adds a regression test that fails without the guard. Fixes READEST-N Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7f85330ae | chore(agent): update agent memories (#4958) | ||
|
|
4527aa277a |
feat(reader): add TTS speak button to dictionary popup (#4876) (#4957)
Add a speaker button to the dictionary popup/sheet header that pronounces the current headword. Tapping it speaks via Edge TTS, falling back to the platform speech engine (Web Speech on desktop/web, native on the mobile app) when Edge is unavailable. To speak as soon as possible, a dedicated wordPronouncer bypasses the reader's TTSController entirely: it never runs EdgeTTSClient.init() (which wastes a round trip synthesizing "test"), calls EdgeSpeechTTS directly (whose static MP3 cache makes repeat words instant), and schedules one chunk on a dedicated Web Audio context isolated from any active read-aloud session. The context is warmed synchronously inside the click gesture so playback is not blocked by autoplay policy after the network await. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6f3b401c24 |
feat(reader): middle mouse button autoscroll in scrolled mode (#4955)
* feat(reader): add middle mouse button autoscroll in scrolled mode Middle-clicking a book in scrolled mode on desktop apps plants an anchor indicator and scrolls with a velocity proportional to the pointer's distance from it, like browser autoscroll (#4951). A quick click sticks until the next click, wheel, or Escape; press-move-release scrolls only while held. Vertical-writing books autoscroll along the horizontal axis. The middle button's default is suppressed while the feature is armed so WebView2's native autoscroll cannot double-drive on Windows. A new Middle-Click Autoscroll toggle in the Scroll settings section (desktop only, default on) turns it off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): drop the middle-click autoscroll toggle Middle-click autoscroll is a common desktop convention and middle click has no other use in the reader, so it is always enabled on desktop apps in scrolled mode instead of being a setting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(i18n): translate missing strings across all locales Fill in translations for 69 keys that landed on main without an i18n pass (search modes, cloud sync and Google Drive settings, page turn animation styles, TTS states, Word Lens hints, file browser sorting, watched-folder auto-import) in all 33 locales, plus the English plural variants for the search result count. Keys the scanner would prune (strings it cannot see statically on this branch) are left untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
da00a94f66 |
feat(sentry): tag events with the WebView engine and version (#4952)
Forwarded browser events carry os/rust/device context but no browser context, so crashes couldn't be correlated with the WebView version. The app now reports its User-Agent at startup via a set_webview_info command; the parsed engine (Chromium/WebKit) and major version are stored and attached as webview.engine and webview.version tags in before_send, covering both Rust panics and forwarded browser events. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
52be6fa066 |
fix(reader): open books without a View Transition to avoid timeout (#4949)
useAppRouter wraps every navigation in a View Transition. Opening a book is a heavy render (the reader mounts and loads the book) that can overrun the transition's ~4s DOM-update budget and abort with a TimeoutError (Sentry READEST-9). Navigate to the reader with the plain router instead, matching every other into-reader path in the app; the transition router stays for lighter navigation. Applies to the tap-to-open flow (useOpenBook) and the post-import queued open (library/page). Fixes READEST-9 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> |
||
|
|
9321c2cd39 |
fix(widget): round iOS cover thumbnail size to whole pixels (#4950)
The iOS reading widget downsampled covers to a fractional target size. UIGraphicsImageRenderer allocates a whole-pixel buffer while draw(in:) fills only the exact fractional rect, so for portrait covers whose scaled width rounds up the rightmost pixel column was left partially covered and semi-transparent. Encoded to JPEG that column flattened into a visible bright hairline along the right edge (intermittent, portrait covers only). Round both target dimensions to whole pixels so the draw rect matches the pixel buffer and every edge pixel is fully covered. Android is unaffected because it scales to a fixed 240x360 and center-crops. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e7f0b53bdf |
fix(opds): crawl subdirectories when auto-downloading directory-style catalogs (#4948)
Copyparty and other file servers expose each folder as an OPDS feed where subfolders are rel="subsection" navigation entries. Auto-download only followed the catalog's "by newest" feed or the subscribed feed itself, so books in subfolders were never discovered, and a folder containing only subfolders was skipped entirely. When a catalog has no "by newest" feed, treat it as a directory-style listing and crawl its subsection navigation entries breadth-first, bounded by MAX_CRAWL_DEPTH levels, MAX_FEEDS_PER_CRAWL fetches, and the visited set. Library catalogs with a "by newest" feed keep the previous behavior and are never crawled. Facet and structural rels (self, up, start, top, search) are excluded so the crawl cannot escape the subscribed folder. Fixes #4272 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3f4d4b8643 |
fix(transfer): persist queue when clearing completed/failed/all (#4947)
Clear Completed, Clear Failed and Clear All mutated the Zustand store directly, so the cleared items were never written back to localStorage. On the next load the persisted queue restored them and they reappeared in the Transfer Queue panel. Route these clears through transferManager (like clearPending already does) so each calls persistQueue() after mutating the store. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9202864846 |
fix: real fix for library-save storage-permission crash + narrowed view-transition filter (#4943)
* fix(library): request storage permission when saving to a custom folder On Android a custom library folder on shared storage needs All Files Access. The import/settings/migrate paths request it, but the library-save path (updateBook/updateBooks -> saveLibraryBooks) did not, so on a device without the permission every save failed with EACCES; because callers (sync, imports) don't await/catch it, it surfaced as an unhandled-rejection crash (Sentry READEST-A). saveLibraryBooks now catches a storage-permission error, requests the permission through the existing flow, and retries once. It prompts at most once per session so background saves don't repeatedly open system settings; a still-denied save is logged rather than crashing (the user was already shown the All Files Access screen). Fixes READEST-A Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sentry): drop only the benign hidden-tab View Transition error The View Transition API skips a transition when the tab is hidden; that unhandled rejection is expected browser behavior and pure noise, so it is dropped in before_send. A transition timeout abort (READEST-9) is NOT dropped: a slow DOM update can signal a real performance problem, so it stays visible. Fixes READEST-7 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2963e75bdd |
fix(sync): propagate group membership for already-synced books (#4946)
Group membership synced only for newly-imported books. Re-grouping a book already present on both devices bumped book.updatedAt and won the library-index LWW race, but mergeBookMetadata dropped groupId/groupName from the overlay, so the change never reached peers. New books instead arrive via addBookToLibrary with the full remote object, which is why their group did travel. Carry groupId/groupName in mergeBookMetadata, matching native cloud sync (transform.ts maps group_id/group_name). Values are assigned raw so a group removal also propagates. Fixes #4942 Co-authored-by: Claude Opus 4.8 (1M context) <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 |
||
|
|
0b180da6a6 |
fix(koplugin): key library pull cursor on synced_at to stop stale library (#4934) (#4944)
The KOReader plugin's incremental books pull went permanently stale: after a while it stopped receiving any updates made from other devices, and only deleting readest_library.sqlite3 + "Pull books now" recovered it (until it re-broke). The iOS/web library was unaffected. Root cause: since #4678 the server keys the books pull on the server-stamped synced_at column, and the web client (computeMaxTimestamp) advances its cursor from synced_at. The koplugin was left on updated_at: pullBooks advanced last_books_pulled_at from max(updated_at, deleted_at). updated_at is client-supplied, and the koplugin bumps it from the device clock (touchBook = os.time()*1000). An e-reader clock ahead of the server (or any row anywhere carrying a future updated_at) drove the cursor past server-now, so the server's synced_at > since filter returned nothing forever. The cursor was also shared between the pull side (compared vs server synced_at) and push-delta detection (getChangedBooks vs local updated_at), so it could not simply be retargeted. Fix: - parseSyncRow reads synced_at; new row_pull_cursor() prefers it and falls back to max(updated_at, deleted_at) for a pre-synced_at server, mirroring computeMaxTimestamp. - Split the cursor: last_books_pulled_at now tracks server synced_at (pull only); new last_books_pushed_at tracks local updated_at for getChangedBooks and is advanced on both pull and push, preserving push dedup. - v2 -> v3 migration seeds the push watermark from the old shared value and resets the pull cursor to 0, auto-healing already-stale installs with one full re-pull (no manual sqlite deletion) and no re-push storm. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
843ab3448b |
feat(tts): keep TTS playing when the book is closed (#4941)
* refactor(tts): controller owns its foliate TTS instance and emits lifecycle events view.close() nulls view.tts, so the controller keeps its own handle (mirrored to view.tts while attached; reads prefer the public mirror). state becomes an accessor that dispatches tts-state-change on a microtask, and terminal conditions (end of content, error exhaustion) fire an explicit tts-session-ended: 'stopped' is a transit value that occurs on every paragraph advance and must never be read as death. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): make TTSController detachable from the reader view detachView enters headless mode: layout-dependent work is guarded, the dead hook's preprocess/section-change closures are severed, and text supply continues through created documents while position events keep flowing. attachView adopts a new view without touching in-flight audio, re-seeding the fresh text instance from the old cursor AT the synchronous swap point (auto-advance during async prep would otherwise replay a paragraph) and aborting via an attach epoch when a detach supersedes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): move media session ownership to a session-scoped bridge ttsMediaBridge binds directly to the controller (metadata per mark, clamped position state, transport handlers, the silent keep-alive element) so the lock screen keeps working when the reader hook is unmounted. The hook's last-writer-wins handler effect and its per-render re-registration are gone; the panel now derives isPlaying from the controller's state channel, so lock-screen transport keeps the in-reader UI truthful. useTTSMediaSession had no consumers left and is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): add hash-keyed TTS session manager with sleep timer and headless persistence Sessions key by book hash (bookKey is regenerated per open), the playback-state relay dedupes transit stopped values so paragraph advances never flicker followers, and terminal handling rides the explicit tts-session-ended event. The sleep timer survives reader unmount, and headless positions persist through the book config on disk (view/progress stores are cleared on close and reopen loads from disk). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): keep TTS playing across book close and reattach on reopen Back-to-library and Android back dispatch tts-close-book (detach when the session is not terminated: transit stopped states during chapter transitions must not kill it); quit and window-destroying closes keep the hard tts-stop so the foreground service tears down with the webview. The unmount cleanup transfers ownership to the manager instead of shutting down, covering deep-link book switches and split-view pane closes. Mounting a book adopts a matching background session once the view is ready (primary pane only) and stops a different book's session unless it is still mounted elsewhere. The sleep timer moves to the manager and a one-time toast announces the first background continuation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tts): now-playing bar in the library for background sessions Floating pill above the shelf while a TTS session outlives its reader: cover, title, sleep-timer countdown, play/pause following the manager-relayed playback channel, and a hard stop. Tapping the body reopens the book in the SAME window regardless of the new-window preference, since the session is a per-webview singleton. Deleting the playing book stops the session before its data is cleared. The bookshelf reserves scroll clearance via a --now-playing-inset var the bar sets while visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tts): make the header close button background-eligible The header X routes through handleCloseBook (onCloseBook), not handleCloseBooksToLibrary, so the sticky eligibility ref never got set and closing a book from the header hard-stopped a live TTS session. Replace the ref with an explicit keepTTSAlive parameter on saveConfigAndCloseBook/handleCloseBooks: back-to-library, Android back, and pane closes pass true; beforeunload, quit-app, and window close invoke handleCloseBooks with an event object, which coerces to a hard stop. This also removes the stickiness where one background close would have made a later quit detach instead of stop. Verified live in Chrome dev-web: close from the header keeps audio playing with the now-playing bar shown; reopening reattaches the same session (generation numbering continues); opening a different book stops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
4dbe9cc9f1 | fix: Sentry production hardening (release/OS tags, unhandled-rejection & render-loop guards) (#4929) | ||
|
|
5301020a02 |
feat(koplugin): pull sync on device wake with book open (#4928)
Waking the device with a book already open now pulls progress, annotations and stats like reopening the book does, instead of requiring a manual sync or book reopen. The pull is delayed 1s so Wi-Fi can come back up after wake (same delay upstream kosync uses on resume), debounced against rapid Suspend/Resume pairs (Android fires them on focus changes), and the pending task is dropped on widget close so it cannot run against a torn-down ReaderUI. Closes #4924 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6013341cb8 |
fix(turso): bump plugin submodule to serialize connection operations (#4927)
Advances the tauri-plugin-turso submodule to include readest/tauri-plugin-turso#2, which serializes execute/select/batch on a single turso connection behind an async mutex. Fixes the "concurrent use forbidden" crash seen in production (unhandled promise rejection in the reader on Android): turso rejects overlapping operations on one connection, and the plugin previously drove a shared connection from concurrent Tauri commands with no serialization. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b403d019e |
feat(calibre): add Readest calibre plugin to push books and metadata (#4918)
* feat(calibre): add Readest calibre plugin to push books and metadata (#4863) Add apps/readest-calibre-plugin, a calibre GUI plugin that uploads selected books with their metadata into the user's Readest cloud library, modeled on BookFusion's open-source plugin. - Selective manual push from the calibre toolbar with per-book status (uploaded / updated / up to date / failed) and quota handling - Books are content-addressed with the same partial MD5 as the apps, so re-pushing updates the existing entry instead of duplicating; metadata edits re-push without re-uploading the file - Metadata mapping includes series, tags, identifiers and optional calibre custom columns; carries over server-side fields (progress, reading status, grouping, cover) that POST /sync would null out - Auth mirrors readest.koplugin and the desktop app: email/password plus browser OAuth (Google/Apple/GitHub/Discord) through a localhost callback server with the fragment-to-query relay - Pure-logic modules (api.py, wire.py, oauth.py) are calibre-free and covered by 56 unit tests (make test); make zip builds the plugin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): package readest-calibre-plugin in releases Mirror the KOReader plugin packaging: a build-calibre-plugin job stamps PLUGIN_VERSION in __init__.py with the release version from apps/readest-app/package.json, builds the zip via make, and uploads Readest-<version>.calibre-plugin.zip to the GitHub release. The version committed in git stays a development placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(agent): add calibre plugin project memory Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(calibre): embed metadata in OPF and dedupe by calibre uuid Rework book identity so metadata can be embedded into the uploaded file without creating duplicates, as requested in review: - Embed calibre metadata (including custom columns) into a temporary copy of the book file at upload time via calibre's set_metadata; the library file is never modified - Dedupe by the calibre book uuid carried in the entry's metadata identifier, which survives file-byte changes, with a live-row preference when both hash and uuid match rows - Detect file content changes via calibreSourceHash, the raw library file fingerprint stored in the pushed metadata, so detection works from any machine; v1 rows fall back to book_hash which equals the raw hash for them - A changed file now replaces the old entry in one sync push (new row with carried-over reading status, grouping, progress and created date, plus a tombstone for the old row) and deletes the old cloud files to reclaim quota - Metadata-only edits still update the library entry without re-uploading the file Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(calibre): set copyright holder to Bilingify LLC Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c86decc2c1 | fix(test): make Android double-tap e2e pass on default-config CI devices (#4921) | ||
|
|
1d3dfd395f |
fix(ios): stop share extension hijacking shared .txt files (#4917)
The iOS share extension is a web-article URL clipper, but its activation rule enabled NSExtensionActivationSupportsText. A .txt file is public.plain-text (conforms to public.text), so that key made the URL-only extension activate for plain-text files it cannot handle: the share sheet hung instead of the file taking the main app's CFBundleDocumentTypes "Copy to Readest" import path, which handles txt fine (like EPUB and PDF, which never matched the extension). Drop NSExtensionActivationSupportsText so the extension activates only for web URLs. Shared .txt files now route to the working document-open import path; sharing a web page URL from Safari or Chrome still works. Add a regression guard asserting project.yml (the xcodegen source of truth for the generated, skip-worktree Info.plist) never re-enables text activation. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
967a7833ca |
feat(sentry): add crash reporting for Android, iOS, desktop, and web (#4914)
Report crashes and unhandled errors from every layer to one Sentry project via a single build-time SENTRY_DSN (empty => disabled, so local and fork builds do not report): - JS/WebView + Rust panics via tauri-plugin-sentry (rustls transport; minidump handler desktop-only). - Android native (JVM/NDK/ANR) via sentry-android 8.47.0 with manifest auto-init (excludes the discontinued lifecycle-common-java8 transitive). - iOS native via sentry-cocoa started from a tracked SentrySupport/+load bootstrap that reads the DSN from a Rust readest_sentry_dsn() FFI (no generated-file edits). - SENTRY_DSN resolved at build time from the environment, then .env.local, then .env (build.rs bakes it via cargo:rustc-env; build.gradle.kts for Android). CI passes the SENTRY_DSN secret through the existing .env.local step. Crashes + errors only: traces sample rate 0, no session replay, no PII. Symbolication (source maps / ProGuard / dSYM upload) is a follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6391bfe788 |
feat(settings): redesign theme mode toggle as a segmented control (#4831) (#4913)
The three theme-mode toggles were small btn-circle btn-sm icons spaced by gap-4, so on mobile they were hard to hit and easy to mis-tap. Replace them with a segmented control: an ARIA radiogroup of three adjacent radio segments sharing one track. Each segment is a full-height tap target (min 44px wide, 36px tall) with no dead space between them, and the active segment gets the app's canonical base-300 fill. In e-ink mode the active segment uses a solid eink-inverted fill instead of a nested border, so it stays legible without a second border clashing with the track outline. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
745f28f346 |
fix(reader): distinguish two-finger scroll from pinch-zoom on touchscreens (#4858) (#4912)
On touchscreen laptops (e.g. Surface), scrolling a fixed-layout book webtoon-style with two fingers moving the same direction accidentally triggered pinch-zoom. The old code committed to a pinch on the first two-finger touch and applied the raw distance ratio from the first move, so a slightly non-parallel scroll drifted the finger spacing and zoomed. Defer the decision with a pending state: on two fingers, compare the change in finger separation against the midpoint travel. A pinch changes separation while the midpoint stays put; a scroll moves the midpoint while separation barely shifts. Zoom only engages once separation change crosses a 24px deadzone and outweighs the pan distance; a 12px pan locks the gesture as a scroll and lets the page scroll natively. On pinch confirm, re-baseline the distance so zoom starts at 1x with no snap. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2b524439bf |
fix(reader): keep running header/footer readable over light PDFs in dark mode (#4901) (#4911)
The running section title and page-number footer used text-neutral-content, which is a light color in dark mode. A light-mode PDF stays white under a dark theme (invertImgColorInDark defaults to false), so the light text sat on the white page and became unreadable. Blend the header/footer text against whatever is behind it using mix-blend-mode: difference with a fixed white/75 anchor, so it inverts to dark on a light page and stays light on a dark margin. white/75 matches the former neutral-content brightness over the dark theme, so reflowable books look unchanged. E-ink keeps its plain base-content text; StatusInfo and the sticky progress bar manage their own colors and are left untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8c91ad411c |
fix(reader): open annotation deep link when a different book is open (#4887) (#4910)
An annotation deep link (readest://book/{hash}/annotation/{id}?cfi=...) for a
book that is not the one currently shown in the reader was ignored: the reader
stayed on the open book. It only worked from the library page.
Two causes, both in the reader-mounted path:
- useOpenAnnotationLink fell through to navigateToReader when the target book
had no live view. router.push to the same /reader route does not re-run the
reader's one-shot init effect, so it was a no-op and the book never changed.
Route it through the in-place switch event (open-book-in-reader) carrying the
cfi, mirroring useOpenBookLink.
- The "already open, jump in place" check scanned all viewStates, which keep
stale entries for books switched away from (their views are detached from the
DOM, never cleared on switch). Switching A -> B -> A matched the stale A view
and called goTo on a dead view. Scope the check to the currently displayed
bookKeys instead.
useBooksManager.openBookInReader now accepts an optional cfi and jumps to it
once the switched-in view is ready (marking it a preview so the saved position
is not overwritten).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c8e2c95335 |
feat(library): auto-import new books from watched folders (#3889) (#4902)
* feat(library): add autoImportFromFolders setting (default off) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(library): add selectNewImportableFiles folder-scan filter Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(library): add useAutoImportFolders trigger hook Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(library): auto-import new books from watched folders on open and focus Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(library): add Auto Import New Books from Folders toggle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(library): don't resurrect deleted books or re-toast bad files on folder auto-import - Add collectKnownSourcePaths() pure helper that includes soft-deleted books - importBooks() accepts { silent } option and returns failedPaths - autoImportFromWatchedFolders uses collectKnownSourcePaths and session-scoped autoImportFailedPathsRef to skip already-failed files on subsequent scans Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(library): make folder auto-import a per-folder option in the import dialog Replace the global autoImportFromFolders toggle (and its standalone Settings menu entry) with a per-folder opt-in shown as a sub-option of Read in place in the Import-from-Folder dialog. Store the watched set as settings.autoImportFolders (a subset of externalLibraryFolders; device-local, backup-blacklisted). The library rescan now iterates autoImportFolders instead of a global-gated externalLibraryFolders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71cb3ace91 |
feat(android): Android Auto media support for TTS playback (#3919) (#4907)
Readest now shows up in the Android Auto launcher as a media app and projects the TTS media session so playback can be controlled from the car display (play/pause, previous/next sentence). - Declare the com.google.android.gms.car.application meta-data and the automotive_app_desc media capability that Android Auto requires to list the app - Make MediaPlaybackService safe to bind for browsing: audio focus, the silent keep-alive player, and the foreground notification no longer start in onCreate but on an explicit ACTIVATE_SESSION command, so a car client connecting to browse does not steal audio focus or post a phantom playing notification - Deactivate the session with an in-process call instead of stopService, which would neither run onDestroy nor clear the foreground state while a media browser keeps the service bound - Serve the current book as a playable browse item (cover downscaled to stay under the binder transaction limit) and handle onPlayFromMediaId/onPlayFromSearch - Honor the foreground service contract when MediaButtonReceiver cold-starts the service with no active session Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4b2c5f93ab |
fix(window): keep Linux window opaque so it can't turn invisible (#3682) (#4904)
On Linux the window was created fully transparent to draw rounded corners (#1982), but on WebKitGTK a transparent window composites as transparent whenever its web process is too busy to repaint damaged regions (for example during a library backup). Interacting with the app then makes it appear to turn invisible, showing the desktop through the window. Make the window opaque everywhere: the main window in lib.rs and the reader/extra windows in nav.ts. Drop the rounded-window treatment (hasRoundedWindow=false) so no rounded 1px border floats on the now square opaque window, and give the Linux loading placeholders a solid background. An opaque window retains its last painted frame instead of going invisible; the tradeoff is square corners on Linux. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
84c5a9dae6 |
fix(window): enter fullscreen from maximized windows (#4034) (#4903)
The fullscreen toggle had an isMaximized branch (from #872) that called unmaximize() and never setFullscreen() when the window was maximized. Phosh windows are always maximized, so the button appeared to do nothing; on Windows it only worked when the window was not maximized. Toggle fullscreen unconditionally. The maximize handler already exits fullscreen first, so the two controls stay consistent. Co-authored-by: Claude Opus 4.8 (1M context) <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> |
||
|
|
fd8fbb178c |
fix(reader): apply page margin changes live on all platforms (#4898) (#4900)
Adjusting the top, bottom, left, or right page margin had no visible effect until an unrelated setting (e.g. Show Header) was toggled. The BooksGrid perf refactor (#4562) memoized the derived view/content insets on the ViewSettings object identity. saveViewSettings mutates ViewSettings in place (same reference), so the memo never recomputed on a margin edit and the new margin never reached the paginator. Left and right margins were always stale; top and bottom only refreshed when the header/footer visibility (an effect dependency) changed, which is why toggling the header appeared to apply a pending change. Extract the inset derivation into useContentInsets and memoize by the resolved numeric values instead of the object reference: identical numbers across a page turn keep a stable reference (no re-render storm), while a changed margin yields a new one that propagates to the renderer. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
77ea87c344 |
fix(updater): disable in-app updater on non-AppImage Linux (#4874) (#4897)
Tauri's Linux updater can only self-update AppImage bundles, so deb/rpm/ pacman and Flatpak installs showed a "Software Update" prompt that could never apply. READEST_DISABLE_UPDATER also had no effect: the variable reached the process, but its value only flowed to the frontend through a WebView init-script global (window.__READEST_UPDATER_DISABLED) that is not reliably visible to page scripts on Linux/WebKitGTK. Make the decision authoritative in Rust and read it over IPC: - Add compute_updater_disabled (pure, unit-tested) plus the is_updater_disabled desktop command: an env opt-out, Flatpak, or a Linux non-AppImage install disables the updater. setup() reuses the same helper for the init-script global. - NativeAppService.init() sets hasUpdater from the command for desktop apps instead of relying on the init-script global. Non-AppImage Linux installs now defer to the system package manager and fall back to the "What's New" release notes. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bd415a8501 |
fix(koplugin): fold duplicate stats book rows so synced time shows in KOReader (#4895)
The stats pull keyed the statistics book table by md5 alone, while KOReader's native statistics plugin keys rows by exact (title, authors, md5). When the two parsers extract slightly different metadata for the same file, the first native open creates a second, zeroed book row that the KOReader UI reads, and the reading time synced from Readest stays stranded on the sync-created row. applyRemote now inserts a book row only for an md5 the DB has never seen, attaches pulled events to the row the native plugin reads (native rows always set pages and last_open; sync-created rows leave pages NULL), and folds never-adopted duplicate rows into the surviving row on every pull, so existing databases heal and the stranded time reappears. Adopted rows and the live session's cached book id are never deleted, and the totals recompute no longer regresses last_open below a real native open timestamp. Fixes #4861 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
849f151166 |
fix(ios): release screen brightness on background so auto-brightness resumes (#4885) (#4896)
On iOS `UIScreen.main.brightness` is a global device setting, not a per-window one like Android. Once Readest overrode it (brightness slider or left-edge swipe gesture) the override survived backgrounding, so swiping to the home screen left the system stuck at an extreme level and ambient auto-brightness appeared locked. The only cleanup lived in the reader's unmount effect, which never runs when the app is merely sent to the background, and the native `brightness < 0` "release" branch was a no-op stub. Native (NativeBridgePlugin.swift): capture the system brightness before the first override, restore it on `appDidEnterBackground` so iOS resumes auto-brightness, and re-apply the app's value on `appWillEnterForeground`. Implement the negative-value release path (restore + forget state), mirroring Android's BRIGHTNESS_OVERRIDE_NONE. JS (useScreenBrightness hook, replacing the racy inline Reader effect): apply the manual brightness while reading, release via setScreenBrightness(-1) on unmount and when "System Screen Brightness" is toggled back on. Excludes screenBrightness from deps so live slider/gesture drags don't flash release-then-reapply. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9f65e3d415 |
fix(auth): surface OAuth callback errors on desktop deeplink (#4881) (#4894)
* fix(auth): handle OAuth callback errors on desktop deeplink (#4881) The Tauri deeplink OAuth handler only parsed the URL hash for an access_token, so error callbacks were silently swallowed and the login screen froze with no feedback. This is how an expired Apple provider secret (GoTrue "Unable to exchange external code") surfaced to users as a dead login screen on macOS and Linux. Extract a pure, tested parseOAuthCallbackUrl() that reads both the hash (implicit-flow tokens) and the query string (provider/GoTrue errors), and route errors to /auth/error before the token branch, matching the web callback page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(scripts): add Apple client secret generator (#4881) Apple caps the "Sign in with Apple" client secret JWT at 6 months, so the web OAuth flow (macOS non-store and Linux) breaks with "Unable to exchange external code" when it expires. This script regenerates the ES256 JWT using Node built-in crypto (no new dependency) for pasting into the Supabase Apple provider config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4d645befde |
feat(library): add "Progress Read" sort option (#4427) (#4893)
Sort the library by reading progress (current/total pages). Books that have never been opened read 0% and sort to the unread end; groups sort by their most-progressed book. Direction reuses the existing ascending/descending toggle, so "most read first" is Descending. Adds the "Progress Read" entry to the Sort by menu and translates it across all locales. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
df34de1c38 |
fix(sync): WebDAV upload-after-enable and deletion propagation (#4856, #4860) (#4892)
* fix(sync): upload book files when Upload Book Files is enabled after first sync (#4856) Incremental sync decided what to push purely from `isLocalNewer` (`book.updatedAt` vs the shared index). A book's config/cover change over time, but its FILE is immutable per hash and only needs uploading once. After a first sync with "Upload Book Files" off, toggling it on never bumped `book.updatedAt`, so the book was skipped and its file never reached the remote. Record which book FILES are already on the remote in library.json (`uploadedHashes`) and split the push decision: config/cover stay gated on the incremental "changed locally" cursor, while a file is (re)uploaded only when syncBooks is on and its hash isn't recorded yet. This keeps an incremental "Sync now" O(changed) — once a file is recorded, later syncs skip it with no per-book HEAD probe, so large libraries don't pay an O(library) cost on every sync. Full Sync bypasses the record as an escape hatch for out-of-band drift. The record is additive and optional, so an old client that rewrites the index just drops it and the next new-client sync re-verifies each file once and re-records it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): propagate WebDAV book deletions to peers and the server (#4860) A book deleted on one device only tombstoned itself in library.json — the deletion never reached other devices or the server: - peers kept the book: the reconcile pass skipped deletedAt entries, so a tombstone never removed the local copy; - the server kept the files: the per-hash directory was never GC'd; - the tombstone could vanish entirely: a device that had never seen the book rebuilt the index purely from its own library, dropping the tombstone and silently reviving the book for everyone. Fixes all three in engine.syncLibrary: - apply a peer's tombstone locally (LocalStore.deleteBookLocally removes the app-managed copy and persists the tombstone), with edit-wins-over-delete LWW so a book still being read isn't yanked; - GC the remote per-hash directory of tombstoned books, scoped to the dirs the discovery scan saw so removed dirs are never re-DELETEd; - union remote-only entries (chiefly tombstones) into the re-pushed index so a deletion can't be dropped by a device that never had the book. Books tombstoned mid-run are excluded from the push pass via the merged state so a just-deleted book isn't re-uploaded right before it is GC'd. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
81802a7c72 |
fix(ios): keep App Group entitlement on widget/share extensions in App Store builds (#4891)
The App Store export re-sign (xcodebuild -exportArchive, automatic signing) stripped com.apple.security.application-groups from the ReadestWidget and ShareExtension binaries because both targets set CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION: YES. The provisioning profiles and source entitlements both grant the group, but the signed extension binaries did not, so the widget read an empty snapshot from the shared App Group container and showed only the placeholder book icon. Dev builds were unaffected. Remove the flag from both extension targets (the main app already ships the group correctly without it) so signing uses the exact CODE_SIGN_ENTITLEMENTS content. Add scripts/verify-ios-appstore-entitlements.sh and run it from release-ios-appstore.sh before upload so a stripped App Group fails the release instead of shipping a dead widget. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a3609731c3 |
fix(macos): minimize instead of hide on macOS 26 to avoid black window (#4890)
On macOS 26 (Tahoe), Apple regressed NSWindow ordering so that orderOut: (what Tauri's hide() maps to) no longer removes the window from the screen. The close-to-hide handler left a focused black phantom window instead of hiding it, and the only recovery was to quit and relaunch the app. This is an OS-level regression, not a Readest bug: the same failure hits native, non-webview apps such as kitty (kovidgoyal/kitty#8952), and tao 0.34.8 calls a bare orderOut with no Tahoe workaround. Fix: on macOS 26 or later, minimize() the main window instead of hide(). Minimize is a different AppKit path that dodges the buggy orderOut, keeps the app in the dock, and preserves the open book. The existing Reopen handler already unminimizes on dock reopen, so no extra restore logic is needed. Older macOS keeps the previous hide() behavior. Version detection reads NSProcessInfo.operatingSystemVersion. Closes #4875 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7a8354d63b |
fix(android): avoid black screen when external cache dir is unavailable (#4889)
The fs capability granted the built-in `fs:allow-cache-read` and `fs:allow-cache-write` sets. Those sets bundle `scope-cache`, which carries the external `$CACHE` base directory. At startup Tauri resolves every granted scope entry, so `$CACHE` resolves through Android's `getExternalCacheDir`. On devices whose external storage volume cannot be prepared (e.g. custom ROMs where `/storage/emulated/0/Android/data/ <pkg>/cache` fails to mkdir) that returns null, the resolve errors, and the graceful "skip unresolvable entry" arm is gated to non-Android, so the error propagates: fs scope build fails, app init aborts, and the window stays black. Readest only performs I/O under the internal app cache (`$APPCACHE` -> `getCacheDir`, always available), so it never needs the external `$CACHE` scope. Grant the scope-free `fs:read-all` and `fs:write-all` command sets to preserve command coverage, and move the `$APPCACHE` scope (plus the iOS container path) into `fs:scope`. Startup then never resolves external storage. Add a regression guard asserting the default capability grants no external-`$CACHE` fs permission while keeping the internal cache scope and full read/write command coverage. Closes #4853 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5bc8eda50b |
feat(proofread): editable Find pattern and per-rule enable/disable toggle (#4859) (#4888)
* fix(proofread): keep disabled book rules visible in the manager list Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(proofread): add per-rule enable/disable toggle in the manager Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(proofread): allow editing Find pattern, regex, and case on existing rules Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n: add proofread edit and toggle strings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
49391124c5 |
fix(reader): correct reading ruler direction for vertical-rl books (#4865) (#4879)
Vertical-rl (Japanese/Chinese vertical) books read top-to-bottom with columns progressing right-to-left, but getDirection only derived rtl from the horizontal dir/direction, which stays ltr for these books. As a result viewSettings.rtl was false and the reading ruler laid columns out left-to-right, advancing the band the wrong way (reverse reading order). Treat writing-mode: vertical-rl as RTL in getDirection so vertical-rl runs through the same rtl paths that horizontal-rtl already uses: the reading ruler coordinate mapping, page-turn tap mapping, footer navigation, and the progress bar. Page-turn taps for these books now follow the vertical-rl convention (tap left to go forward), matching horizontal-rtl behavior. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |