f8916e128e4410e198956976e6c8af60690a093d
2448 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
7e78f80e14 |
feat(sync): Google Drive cloud sync + premium Third-party Cloud Sync section (desktop) (#4821)
* feat(sync): add Google Drive file-sync provider core Second FileSyncProvider for the merged provider-agnostic file-sync engine, behind the provider seam. This is the CI-testable core only: no settings UI and no platform OAuth runners yet (those land in later phases). - GoogleDriveProvider over the Drive v3 REST API: id-addressed path resolution with a per-instance id cache, create-then-name uploads, real idempotent ensureDir, files.list pagination, Retry-After-aware 429/5xx backoff, per-path folder-creation locks with deterministic duplicate collapse, stale-id eviction, and FileSyncError mapping (403 split into rate-limit vs permission). - DI OAuth layer: pkce, parseRedirect (redirect-target + CSRF state), reverseDnsRedirect, tokenStore (iOS client, no secret), oauthFlow. - PersistedDriveAuth with single-flight token refresh; keychain-backed token store with no ephemeral fallback for the refresh token; account label via about.get. - providerRegistry (backend kind to provider) and buildGoogleDriveProvider assembly. - Shared transport-agnostic provider semantic contract, run against both WebDAV and Drive. - Keyed secure-KV bridge contract (set/get/clear_secure_item); the native keychain implementation lands with the desktop OAuth slice that first exercises it. Adapted from ratatabananana-bit/Readest-google-drive-mod-patcher (AGPL-3.0) with the author's explicit permission. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): multi-provider file-sync settings + sync-state foundation PR2 foundation for a second file-sync backend (Google Drive). The behaviour-sensitive reader-hook and Sync-now form generalization land in PR3 alongside OAuth, where Drive actually connects and the multi-provider paths can be exercised and live-verified (and the extracted form gets its second consumer, avoiding a single-use abstraction). - GoogleDriveSettings type (mirrors WebDAVSettings minus URL/credentials/ rootPath, plus accountLabel) wired into SystemSettings, with DEFAULT_GOOGLE_DRIVE_SETTINGS in the defaults. - googleDrive.deviceId + googleDrive.lastSyncedAt added to the backup blacklist so device-local sync identity / cursors never restore onto another device. Covered by the existing backup-settings test. - Generalize webdavSyncStore into fileSyncStore: per-backend progress keyed by provider kind, plus a global library-sync mutex (beginSync returns false when another backend already holds the lock) since every backend's syncLibrary mutates the same local library. Migrate WebDAVForm and IntegrationsPanel to the keyed API; WebDAV behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(native-bridge): add keyed secure key-value store commands A generic, keyed secret store over the same OS keychain backends as the sync passphrase (set/get/clear_secure_item), so secrets that aren't the single sync passphrase get the same XSS-free cross-launch persistence without each needing its own native command. The Google Drive OAuth token store (PR1's KeychainTokenPersistence) is the first consumer; a future cloud provider's refresh token reuses it. - Desktop (macOS/Windows/Linux): keyring-core, keyed by the item key as the entry account under the existing "Readest Safe Storage" service. - Android: EncryptedSharedPreferences (a dedicated readest_secure_items_v1 file, the item key as the pref key). - iOS: Security framework Keychain (kSecClassGenericPassword, dedicated service, the item key as kSecAttrAccount). Registered in the plugin invoke handler + build COMMANDS + default permission set (autogenerated permission files regenerated; the passphrase entries are preserved). The TS bridge wrappers shipped in PR1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): desktop Google Drive OAuth runner + connect flow The desktop half of Drive sign-in: open consent in the system browser, capture the reverse-DNS redirect the OS routes back, and exchange the code for tokens. - oauthDesktop.ts: runDesktopDeepLinkOAuth wires the DI OAuth flow to the desktop mechanics (open default browser, capture via single-instance / onOpenUrl, cold-browser fallback after a grace period, hard deadline). Fully headless-unit-tested via injected deps. - spawn_fresh_browser.rs (+ registration, Windows-only winreg dep): the cold browser the runner falls back to when the user's already-running browser snapshotted protocol associations before the scheme was registered (a Windows-specific failure). Resolves the default browser from the registry and spawns it cold with an isolated --user-data-dir; a no-op on macOS/Linux where the default-browser open already routes the redirect. Pure helpers unit-tested. - connectGoogleDrive.ts: run the platform OAuth runner, persist the token (fail-loud — Drive is not reported connected if the refresh token does not save), and resolve the account label via about.get (best-effort). OAuth runner adapted from ratatabananana-bit/Readest-google-drive-mod-patcher (AGPL-3.0) with the author's permission. Scheme registration + the ingress redirect filter + the Drive connect UI land in the following commits; live desktop verification follows once the official Google client id is provisioned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): filter Google OAuth redirects out of the deep-link ingress The reverse-DNS OAuth redirect (com.googleusercontent.apps.<id>:/oauthredirect) is delivered through the same single-instance / onOpenUrl channels as book-file deep links. Without a filter the book-import consumer would treat the redirect URL as a file path to open. Drop it at the ingress source (useAppUrlIngress) before the app-incoming-url broadcast, so no consumer ever sees it; the Drive sign-in runner still captures it via its own listeners. isGoogleOAuthRedirectUrl matches the scheme prefix (not a specific client id), so it stays correct regardless of which client is baked into the build. Note: registering the scheme in tauri.conf.json (so the OS routes it back to the app) needs the official Google client id, which is a provisioning prerequisite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): bake the official Google Drive OAuth client id + redirect scheme Provisioned the Readest Google Cloud OAuth client (iOS application type, no secret, drive.file scope). Bake the client id as the default in getGoogleClientId (overridable via NEXT_PUBLIC_GOOGLE_CLIENT_ID for forkers, who must also regenerate the manifest schemes) and register the derived reverse-DNS redirect scheme com.googleusercontent.apps.<id> in tauri.conf.json (desktop + mobile deep-link) so the OS routes the OAuth redirect back to the app. The client id is a public client identifier, not a secret. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): Google Drive connect UI + shared FileSyncForm Make Drive usable from Settings, and extract the now-two-consumer sync controls. - FileSyncForm: the provider-agnostic sync controls (sub-toggles, conflict strategy, manual "Sync now" with progress + result toast), parameterised by backend kind and building the provider through the registry. Extracted from WebDAVForm now that a second consumer exists. WebDAVForm keeps its URL/credentials connect panel + browse pane and renders FileSyncForm for the sync section; behaviour is unchanged (WebDAV "Sync now" goes through the same provider via the registry). - GoogleDriveForm: an OAuth connect panel (Connect -> runGoogleDriveConnect -> store token in keychain -> "Connected as <email>"; Disconnect) + FileSyncForm. - googleDriveConnect.ts: assemble the env client id + keychain + desktop runner into connectGoogleDrive/disconnectGoogleDrive for the UI. - IntegrationsPanel: a "Google Drive" row + sub-page, shown only on desktop (mobile OAuth runners land in later phases). Reader-side auto-sync (generalizing useWebDAVSync) is a follow-up; manual "Sync now" already exercises the full Drive stack. Full suite 6412 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): unified Third-party Cloud Sync section (exclusive provider) Group WebDAV + Google Drive into a new "Third-party Cloud Sync" section and make them mutually exclusive — only one cloud provider syncs the library at a time. - New unified "Cloud Sync" sub-page (CloudSyncForm): a provider picker (radio, the AIPanel mutually-exclusive pattern) on top, the shared FileSyncForm sync options below for whichever provider is active. Google Drive is offered only on desktop; on mobile the page is WebDAV only and the picker is hidden. - withActiveCloudProvider helper: enabling one provider disables the other in one save. Both panels' connect/activate paths use it. Unit-tested. - WebDAVForm / GoogleDriveForm refactored into embeddable panels (the unified page owns the header). Drive gains a "configured but inactive" state so switching back re-activates it without a fresh sign-in; explicit Disconnect clears the keychain token. - IntegrationsPanel: remove the two separate WebDAV / Google Drive rows from "Reading Sync" (now KOReader Sync / Readwise / Hardcover only); add the Third-party Cloud Sync section with one Cloud Sync row (status = active provider). Old webdav/gdrive deep-links route to the unified page. Also removes the temporary Drive concurrency probe (the upload already runs at the intended concurrency 4; the probe confirmed it). Full suite 6416 green; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): auto-sync the active cloud provider while reading Generalize the reader sync hook from useWebDAVSync to useFileSync so the active third-party cloud provider (WebDAV OR Google Drive) syncs per-book while reading — pull-on-open, debounced push on progress/booknote changes, cover/file upload — not just via the manual "Sync now" in settings. Since the providers are mutually exclusive, the hook drives exactly the one enabled backend, built through the provider registry. The build is async (the Google Drive provider probes the OS keychain), so the engine lives in state and the pull-on-open waits for it; switching providers mid-session resets the per-book locks. The engine is keyed on connection-relevant settings so a lastSyncedAt write doesn't re-probe the keychain. deviceId / lastSyncedAt now write the active provider's settings slice; the auth-failed toast is provider-neutral; the per-book events are renamed *-file-sync. WebDAV reader-sync behaviour is unchanged. Full suite 6416 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): surface cloud providers in the section with inline switch Show WebDAV + Google Drive as separate rows in the Third-party Cloud Sync section (instead of one "Cloud Sync" row), so both providers are visible and the active one can be switched right there. - CloudProviderRow: a trailing radio makes a provider the single active sync target inline (enabled only when it's already configured — WebDAV creds / a Drive token); the row body / chevron opens its config sub-page (connect, sync options, disconnect). Status reads Active / Configured / Not connected, with a Syncing… indicator. - Each provider drills into its own sub-page again (WebDAV / Google Drive), rendering the embeddable panel under a SubPageHeader; the brief unified CloudSyncForm picker page is removed (its old deep-link maps to Google Drive). - Switching stays exclusive via withActiveCloudProvider; an inline switch trusts the stored credentials/token (no re-validate / re-OAuth). Full suite 6416 green; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): gate third-party cloud sync behind a premium plan WebDAV + Google Drive sync is now a premium feature: available on any paid plan (Plus, Pro, or Lifetime), not on free. - isCloudSyncInPlan(plan) helper (mirrors isEmailInPlan; plus/pro/purchase). - IntegrationsPanel: free users see the Third-party Cloud Sync section with an upgrade row ("Available on Plus, Pro, or Lifetime") that opens the plans page instead of the provider rows; the cloud-sync deep-links are gated too (waiting for the plan to load before deciding). - useFileSync: the reader's auto-sync only runs on a paid plan, so a downgraded user's sync stops even if a provider's enabled flag lingers. Full suite 6418 green; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): escape backslashes in Drive query literals (CodeQL) escapeDriveLiteral escaped single quotes but not the backslash escape character, so a file name containing a backslash (or ending in one) could break out of the single-quoted Drive `files.list` query literal and malform the query. Escape backslashes first, then single quotes, so the backslashes added for the quotes are not doubled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9496de301b |
fix(node-app-service): ensure correct cross-platform path resolution in NodeAppService (#4819)
* refactor(node): use path.join() in path resolver * fix(node): use native path separators in resolveFilePath. |
||
|
|
348c85f648 |
fix(reader): cap auto page-turn corner zone size (#4812) (#4820)
The corner-dwell auto page-turn zone is a quarter-ellipse whose radius is a fraction (0.15) of the reading area on each axis. On wide screens such as desktop or multi-column pages, that fraction grows the zone until it reaches deep into the text, so selecting in a column and resting the pointer there turns the page unexpectedly. Cap each axis of the corner radius at 50px so the engagement zone stays a real corner regardless of page width, while preserving the existing feel on phones. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e93445336 |
fix(sync): sync WebDAV credentials across devices (#4810) (#4818)
WebDAV connection settings were never part of the bundled settings replica, so the "Credentials" sync toggle had no effect on them and users had to re-enter their WebDAV server, username, and password on every device. Add webdav.serverUrl / username / password / rootPath to SETTINGS_WHITELIST and gate username / password behind SETTINGS_ENCRYPTED_FIELDS, matching how KOSync / Readwise / Hardcover credentials are handled. Per-device bookkeeping (enabled, deviceId, lastSyncedAt, sync sub-toggles) stays local, mirroring KOSync which syncs credentials but not its enabled flag: a fresh device pre-fills the connect form and the user clicks Connect. Also add a webdav deep-merge case to mergeSettings. Without it the top-level shallow merge on pull would replace the whole webdav object with the four-field patch and wipe the local per-device fields. Update the credentials category description to mention WebDAV and migrate the i18n key across all locales. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
24370ca511 |
feat(reader): render Markdown (.md) files at runtime (#774) (#4816)
Open standalone .md files in the reader without converting to EPUB. A new makeMarkdownBook (src/utils/md.ts) parses Markdown to sanitized HTML with marked + DOMPurify, splits the document into sections at H1 boundaries, and builds an in-memory foliate book (modeled on fb2.js) with a nested heading TOC. DocumentLoader routes .md/.markdown before the TXT path so a Markdown file served as text/plain is not converted to EPUB. Layout, font and theme settings apply the same as for any other format. Relative-image resolution and Markdown bundle/folder packages are left as follow-ups (a standalone file has no sibling-asset access on the web). 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> |
||
|
|
97868f0486 |
fix(reader): keep negative table margins from clipping wrapped layout tables (#4439) (#4808)
A decorative table-of-contents page lays out as nested tables where the inner table pulls itself up with a negative top margin and the CONTENTS heading uses line-height:1em. Since #4400 wraps every table in a `.scroll-wrapper` (overflow:auto), that negative margin bled the heading above the wrapper's clip box and overflow cut off the top half of its glyphs. Hoist any negative margins from the wrapped element onto the wrapper and zero them on the element: the box stays in place, the element sits flush inside it so overflow cannot clip it, and scrollWidth is no longer inflated by the margin so a table that actually fits still gets marked fit. Positive and auto margins are left alone, so an over-wide table still scrolls and a centered table stays centered. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4874eb9ae7 | feat(reader): add TTS highlight granularity setting (word or sentence) (#4807) | ||
|
|
dced42912f |
feat(reader): filter exported annotations by color and style (#4801) (#4806)
Add a Filter section to the annotation export dialog so users who color-code highlights (e.g. red for important, yellow for difficult words) can export only selected colors and styles. The selection is stored as exclusions in NoteExportConfig, so an empty filter exports everything and any color or style added later is included by default. A new pure helper filterExportGroups applies the filter to both the default formatter and the custom-template paths, and only filters a dimension when at least two distinct values are present so a hidden row never silently drops notes. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
01a54238ae |
fix(annotator): clean up empty highlight on annotation cancel (#4791) (#4804)
Clicking "Annotate" on a selection eagerly creates a highlight (with an empty note) as the anchor for the note being typed, so the selection stays visible while the NoteEditor is open. Cancelling the note instead of saving left that empty highlight behind: it leaked into the config DB, showed as a stale card in the Booknotes list, and left a phantom yellow highlight. handleHighlight now returns the created BookNote only when it pushes a new record (null when it restyles an existing highlight, which predates the flow and must survive a cancel). handleAnnotate tracks that id via the new notebookNewHighlightId store field; cleanup is keyed on the id, not the cfi, so a fresh selection that collides with an existing highlight's cfi can't wrongly delete it. removeEmptyAnnotationPlaceholder tombstones the tracked placeholder only when it still has no note text, and the Notebook tears its overlay down. Cleanup is presentation-driven: an effect removes the placeholder whenever the creation editor stops being shown (Cancel, Escape, overlay, close, swipe, navigate), plus a second effect for book-switch and reader-close. Save survives the guard and clears the tracked id. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1558078391 |
fix(settings): keep global settings in sync across windows (#4580) (#4803)
On desktop the app runs multiple windows (one library plus one per open book), and each keeps its own in-memory settings loaded once at window open. Global settings persist to a single shared settings.json, and every window writes the whole object on save. A window opened before the user customized a global view setting therefore clobbers that change with its own stale (often default) value on its next save, most visibly a reader window reverting Click to Paginate back to the default on close. Broadcast the global view and read settings after every save and have all other windows adopt them, preserving each window's device-local fields (paths, lastOpenBooks, sync cursors, brightness). The receive path only updates the in-memory store, so there is no save or broadcast loop. No-op off Tauri. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7544835fb8 | chore(agent): update agent memories (#4802) | ||
|
|
4ba78490a7 |
fix(library): prevent series and description overlap in list view (#4796) (#4799)
The list-mode book item used a fixed h-28 height. When a book belongs to a series, the title, authors, series, description, and progress row together exceed 112px and overflow, so the series and summary lines collide and get clipped. Larger system font scaling (such as the Android accessibility font size setting) inflates line heights and makes the overlap worse, which is what the reporter saw on a Pixel 10 Pro. Use min-h-28 instead so the row grows to fit its content. Non-series rows keep the same 112px height, and the list is virtualized with measured heights so variable row heights are fine. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0b4993407c |
feat(reader): add contrast option to PDF/CBZ view menu (#4800)
Add a Contrast stepper to the reader View menu for fixed-layout (PDF/CBZ) documents. It increases and decreases page contrast via a CSS filter on the rendered page images, applies to the whole book, and is stored per-book (local to the current document). The filter is built in applyFixedlayoutStyles by combining any dark-mode invert with the contrast amount into a single filter declaration. Persisted with skipGlobal so it never touches global view settings, and added to FoliateViewer's effect dependencies so the change re-applies across all rendered pages. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
370a516620 |
feat(reader): glue non-breaking spaces after short Russian words (#4769) (#4798)
Russian typography requires short function words (prepositions, conjunctions, particles) to never hang at the end of a line. Add an `nbsp` content transformer that inserts U+00A0 after such words so they stick to the following word. The source file is never modified. The transformer is language-driven via an NBSP_LANGUAGES registry keyed by language code (only `ru` is configured today), so adding another language is a single entry. It runs only for matching books and rewrites text between tags with a regex, leaving tags, attributes, and the XML declaration intact. Runs after whitespace normalization so the inserted spaces are not stripped under the override-layout setting. The space-to-NBSP swap is length-preserving (both are single UTF-16 code units), so DOM character offsets and CFIs stay valid for every word before and after the transform; tests enforce this invariant. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4c39d769e6 |
fix(hardcover): never send a book id as edition_id (#4792) (#4794)
When a book was matched via Hardcover title search with no featured
edition and the user had not selected a specific edition, the sync
client fell back to using the Hardcover book id as the edition_id.
Hardcover's Action rejects that with a parse-failed error
("ActionWebhookErrorResponse ... key 'message' not found"), so progress
and note sync failed for those books.
Leave editionId null when no real edition is known, make the read and
journal mutations accept a nullable edition_id, and omit edition_id when
adding a book so Hardcover uses the book's default edition.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
58f84d18c1 |
fix(sync): keep WebDAV connection after restart when a pull overlaps it (#4793)
useSync.pullChanges already re-read the live store settings for its in-try setSettings, but its catch and finally still wrote the stale per-render hook closure. When a settings change lands during an in-flight pull (most visibly a WebDAV connect), the pull's finally overwrote settings.json with the pre-change snapshot, so the connection read back as "Not connected" after the app was reopened. WebDAV was the unique casualty because it is the only integration credential not in the replica SETTINGS_WHITELIST, so unlike kosync/readwise/hardcover it is never re-hydrated from the server on the next launch. Android's slower network widens the pull window, which made the overlap reliable there. Read useSettingsStore.getState().settings in both the catch and the finally, matching the in-try path. This is a general fix that preserves any concurrent settings change, not just WebDAV. Adds a regression test that drives the real hook with a connect landing mid-pull. Fixes #4780 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
13e0fb814f |
feat(webdav): sort and filter the WebDAV browser (#4724) (#4786)
Add per-folder sort and search to the WebDAV browse pane in Settings, Integrations, WebDAV. - Sort by name, date modified, date created, or size, ascending or descending; the choice persists in WebDAV settings so a chosen "recent first" order survives across sessions. - Filter the current folder by file name or matched book title. - Request and parse the WebDAV creationdate property; servers that omit it fall back gracefully to a stable name order with no broken dates. - Sort and search resolve a per-hash book directory to its library title so they operate on what the user actually sees. Sort and filter are pure, unit-tested helpers in webdavBrowseUtils; creationdate parsing is covered by a listDirectory test. Verified on a Xiaomi device against a live WebDAV server (675 books): name, modified asc/desc, title filter, and persistence across an app restart. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fb943987eb |
fix(opds): hide popular catalog after adding it to My Catalogs (#4782) (#4787)
Adding a built-in popular catalog (e.g. Project Gutenberg) to My Catalogs left it still rendering in the Popular Catalogs section, so it looked like a duplicate. Only the card's Add button was hidden; the card itself stayed. Filter added (and disabled) entries out of the Popular list entirely via a new pure helper getUnaddedPopularCatalogs, which matches by normalized URL (trim + lowercase) to mirror the store's findByUrl dedup. The section already auto-hides when the list is empty, so it disappears once all are added. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
99b9adfe85 |
refactor(sync): provider-agnostic file-sync engine with incremental WebDAV sync (#4784)
* refactor(sync): extract provider-agnostic layout paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): extract wire envelope module Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): extract pure merge module with law tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): add FileSyncProvider and LocalStore interfaces Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): FileSyncEngine orchestration over a provider Port WebDAVSync's per-book + library-wide sync onto FileSyncProvider + LocalStore. Behavior preserved; the #4756 metadata-reconciliation test is retargeted to drive the engine through a fake provider + store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): move WebDAV client + connect settings under providers/webdav Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): WebDAVProvider implementing FileSyncProvider Wraps the WebDAV transport client, maps WebDAVRequestError to the neutral FileSyncError, and owns Tauri streaming upload/download. Adds a provider-conformance suite future backends can run against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): shared appService-backed LocalStore bridge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reader): drive WebDAV sync through FileSyncEngine Construct a WebDAVProvider + shared LocalStore + engine once per hook; the inline buffered/streaming book-file loader collapses into the provider + store, so the hook no longer imports tauriUpload or the file path helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(settings): drive WebDAV library sync + browse through the provider WebDAVForm now builds a WebDAVProvider + shared LocalStore + engine and calls engine.syncLibrary; the ~170-line inline callback block (buffered/streaming loaders, URL+auth construction) is gone. WebDAVBrowsePane builds a provider for the engine-level deleteRemoteBookDir cleanup helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): remove WebDAV-specific sync module, WebDAV is now a provider Delete src/services/webdav (WebDAVSync/WebDAVPaths + the transitional client and connect-settings shims). The superseded webdav-metadata-sync test is replaced by engine-metadata-sync; webdav-delete now drives deleteRemoteBookDir through a WebDAVProvider and asserts FileSyncError. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): hydrate library before WebDAV Sync now to prevent clobber Sync now while the library store was unloaded (app launched into reader/ settings without mounting the Library view) merged the engine's addBookToLibrary / updateBookMetadata against an empty in-memory library, persisting a downloaded book or a metadata update as the entire library and wiping what was on disk. Pre-existing bug surfaced during the file-sync review. Hydrate the store in handleSyncNow and harden the store bridge with a load-if-unloaded guard (mirrors useLibraryStore.updateBooks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): make listDirectory honor the FileSyncError contract listDirectory threw a plain Error (and let raw fetch failures escape), so WebDAVProvider flattened every list() failure to FileSyncError(UNKNOWN). Throw the same WebDAVRequestError taxonomy as the file-level helpers (AUTH_FAILED / NOT_FOUND / NETWORK) so the provider maps them correctly. Add list() cases to the provider-conformance suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sync): cover streaming upload, discovery/download, and receive paths The metadata-sync gate only exercised the buffered metadata + config-merge paths. Add engine tests for streaming uploadStream (+ HEAD short-circuit + one-shot retry), remote-only discovery -> streaming download -> addBook, and the receive strategy (pull-only, no config or index writes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): incremental WebDAV Sync now + bounded concurrency Sync now was a full walk of every book each run (675 round-trips even when nothing changed). Default to incremental: diff the local library against the shared library.json index per hash and only process books whose local copy is newer (or absent). book.updatedAt bumps on every progress/notes/metadata save (bookDataStore.saveConfig), so the index is a reliable per-book change marker. Remote-newer books pull their config in the reconcile pass so peer progress still propagates. A new 'Full Sync' toggle (default off) re-checks everything. Also run the reconcile / download / push phases over a bounded worker pool (default concurrency 4) instead of one book at a time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): simplify Sync now toast to a single book count The completion toast built a multi-line success bullet list (downloaded / pulled / pushed / uploaded). Replace it with the same single-line info toast the native cloud sync uses: '{{count}} book(s) synced'. Add a booksSynced counter to the engine result (a Set of distinct hashes touched in any direction, since the per-action counters overlap under Full Sync). Failures still surface as a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): raise toasts above modals so they aren't hidden by open dialogs Toasts rendered at z-50, below the Settings dialog (z-110) and ModalPortal (z-120), so a toast dispatched from an open dialog (e.g. WebDAV 'Sync now') was buried. The documented overlay scale already places toast at 130; the component just hadn't followed it. Move the toast to z-[130] and extend the zIndexScale invariant test to guard TOAST > MODAL/SETTINGS and APP_LOCK > TOAST. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
79ae8a48ba |
feat(reader): sync per-book proofread rules across devices (#4781)
Per-book and selection-scope proofread (find/replace) rules were pushed in the synced book config but dropped on pull (applyRemoteProgress only applied location), so they never propagated across devices. Merge them by id on the config pull, mirroring the booknote CRDT path. Library-scope rules keep syncing via the settings replica. - Add updatedAt/deletedAt to ProofreadRule. Delete is now a tombstone for book/selection scope so a removal is not resurrected by a peer's live copy; library-scope deletion keeps the hard splice (settings-replica whole-field LWW already handles it). - Add mergeProofreadRules (by id, updatedAt/deletedAt last-write-wins) and merge into applyRemoteProgress; refresh the live view only when the merged rules actually changed. - Backfill a content-derived id for id-less rules (legacy/foreign/hand-edited) via ensureRuleId, and seed book/library ids from content so the same rule created on two devices dedupes instead of duplicating. Selection rules keep a per-instance unique id. Without this, id-less rules collide on one Map key and clobber each other. - Filter tombstoned rules from the transformer and the manager dialog list. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0589cb4f4a |
fix(reader): stop a quick-deleted highlight from being re-drawn (#4773) (#4779)
The per-relocate re-apply effect reads a memoized annotation index. A highlight deleted in place after the index was built still sits in its bucket, and selectLocationAnnotations trusted the build-time deletedAt filter, so the effect re-drew the just-deleted overlay and left it orphaned on the page until the book was reopened. Re-check deletedAt at the read site: in selectLocationAnnotations and in the sibling globals re-apply loop in Annotator. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cecb1c5312 |
chore(deps): bump the github-actions group with 2 updates (#4775)
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/cache](https://github.com/actions/cache). Updates `actions/checkout` from 6.0.3 to 7.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) Updates `actions/cache` from 5.0.5 to 6.0.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...2c8a9bd7457de244a408f35966fab2fb45fda9c8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.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> |
||
|
|
e80ab1762b |
refactor(settings): polish sync and integration panels (#4774)
- Background Image: move the Library/Reader scope into the section title
("Background Image (Library)" and "Background Image (Reader)") instead
of a separate "Applies to ..." sublabel line.
- Send to Readest: render approved-sender emails monospace to match the
inbound address, and wrap long addresses to at most two lines instead
of truncating on one line.
- WebDAV: split the "Uploading X / Y" progress into a status line plus a
one-line book title.
- WebDAV: reword the "Upload Book Files" description to "Uploads book
files to your other devices."
- WebDAV: rename the "Always use latest" strategy to "Send and receive".
KOSync keeps "Always use latest" since it must contrast with its
"Ask on conflict" option.
- WebDAV: remove the Sync History section and its persisted log model;
the sync engine still reports per-book failures in its result.
Updated i18n across all 33 locales.
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> |
||
|
|
44a6900da0 |
feat(reader): extend selections and highlights across pages (#4741) (#4767)
* docs(plan): design for cross-page corner auto-turn (#4741) Extract useAutoPageTurn so the corner-dwell page turn works for instant highlight drags and for range-editor handle drags, not just native text selection. Decouple the dwell liveness from the DOM selection and anchor each range's non-dragged end to a DOM position so it survives the scroll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plan): add keyboard turn-on-cross to cross-page design (#4741) Shift+Arrow selection adjust extends into the off-screen next column without turning the page. Fold it into the feature with an immediate turn-on-cross (no dwell) in the keyboard path, reusing the page-edge geometry from useAutoPageTurn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): extend selections and highlights across pages (#4741) Extract the corner-dwell auto page-turn (#1354) into useAutoPageTurn, decoupled from the DOM selection, so every selection gesture can drive it in paginated mode, not just native text selection: - Instant Highlight drag: feed the finger corner into the dwell machine and DOM-anchor the highlight start so it survives the page scroll. - SelectionRangeEditor and AnnotationRangeEditor handle drags: feed the dragged-handle corner; anchor the non-dragged end to a DOM position so the edited range spans pages (the annotation editor previously resolved both ends from window coordinates and lost the previous page). - Shift+Arrow keyboard selection adjust: turn the page immediately when the extended focus leaves the visible page, so the growing selection stays in view. An after-turn re-emit rebuilds each gesture's range from the held position so the selection extends onto the new page without waiting for the next move. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d963b911c8 |
fix(reader): zoom linked images on single tap (#4757) (#4766)
A single tap on an image wrapped in an <a> element followed the link instead of opening the image viewer, because postSingleClick returned early for any element inside an anchor before reaching media detection. Compute the media target up front and let it bypass the anchor guard, so a tapped image/table/svg-image opens the viewer just like long-press already does. Footnotes are excluded so footnote anchors keep their popup and navigation behavior. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e0b537bc16 |
feat(koplugin): bulk download all cloud books from Library view (#4751) (#4765)
Downloading a Readest cloud library into KOReader previously required tapping each book one at a time. Add a "Download all books" action to the Library view menu that pulls every cloud-only book to the device in one pass. - LibraryStore:listCloudOnlyBooks() returns the downloadable cloud-present, not-local books for the current user (whole library, independent of the active search/group view). - librarywidget.downloadAll() streams them sequentially via the existing syncbooks.downloadBook path inside a Trapper coroutine: cancellable progress (Trapper:info Abort/Continue), per-book failures skipped and counted, summary toast at the end. Bridges downloadBook's sync-or-async callback with a coroutine suspended check so it serializes correctly either way. - Wire the action into the view-menu Actions section. - Add the new UI strings and translate them across all 33 locales. Closes #4751 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> |
||
|
|
f7124cbeea | fix(css): multiply mix blend for images in dark override color mode (#4763) | ||
|
|
005aa2d615 | fix(security): iframe srcdoc atrribute can lead to arbitrary code execution (#4762) | ||
|
|
7da5f83213 |
fix(reader): make annotation toolbar customization apply to all books (#4760)
The customized annotation toolbar only took effect in the book where it was changed, instead of applying to every book. Root cause: serializeConfig decided which per-book view settings to persist as overrides using a reference check (globalViewSettings[key] !== value). It deep-clones the config first, so array-valued settings like annotationToolbarItems are always a fresh reference and were stored as a per-book override on every save (each progress autosave). On reopen the per-book override shadowed the global value, so a global toolbar change never reached already-opened books. Compare view-setting values by content, not reference, so array/object settings equal to the global value are no longer persisted per-book. This also fixes the same latent issue for paragraphMode, proofreadRules, ttsHighlightOptions and noteExportConfig. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ac6249cbc3 |
feat(opds): show groups as horizontal carousels when 2+ groups (#4750) (#4755)
Feeds that list several groups previously rendered each group as a full grid, which made scrolling past many groups tedious. When a feed has two or more groups, render each group's items in a compact horizontal carousel, matching what Thorium does. Each carousel is a horizontally virtualized react-virtuoso list, so only the covers in view are mounted and fetched; off-screen covers load lazily as the row is scrolled. Scroll arrows page through by index and stay centered on the cover artwork. Book items also get rounded covers (matching the library bookshelf) and drop the inline acquisition badge, which remains on the publication detail page. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7d1a60b9ea |
feat(library): separate background texture for library and reader (#4754)
* feat(library): separate background texture for library and reader (#4743) The library and reader shared a single background texture, so a reader backdrop with borders or other reading-oriented decoration looked wrong on the bookshelf. Let users set them independently. - Add device-local libraryBackground{TextureId,Opacity,Size} to SystemSettings. Each field falls back to the reader/global value when unset (getLibraryViewSettings), so an existing bookshelf looks unchanged until the user picks a library texture, then decouples per-field. No migration needed; the selection stays per-device like the reader's, while imported images keep syncing via the texture kind. - Make the Color panel's Background Image picker context-aware: opened from the library it edits the library texture, opened while reading it edits the reader texture. A sublabel states which page it applies to. - Apply the library texture at boot and on every library mount, so returning from a textured book restores the bookshelf background. - useBackgroundTexture now unmounts on 'none' instead of early-returning, since library and reader share one style element: switching a page to None must clear a texture the other page mounted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n: translate library/reader background texture labels (#4743) Add translations for the two new context sublabels ("Applies to the Library" / "Applies to the Reader") across all 33 locales, anchored to each locale's existing Library and reading terminology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e2f65278ec |
fix(opds): dereference publication self link for full metadata (#4749) (#4753)
OPDS 2.0 feeds may list a publication in summary form (title, cover, and only a rel="self" link of type application/opds-publication+json), serving the full record (acquisition links, description, publisher, subjects) only when the client follows that link on click, as Thorium does. Readest ignored it, so such books showed no download option and no description. Add opdsPublication.ts with getPublicationDetailHref and parsePublicationDocument (OPDS 2.0 JSON and Atom entry, absolutizing link/image hrefs), and dereference the link in the OPDS page when a feed-selected summary advertises one, upgrading the detail view in place once it loads. Also render an OPDS 2.0 JSON HTML description as sanitized markup instead of literal tags by falling back from the typed content to the plain description string in getOPDSDescriptionHtml. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8810aa6db0 |
fix(reader): stop trackpad pinch-zoom flicker on image viewer (#4742) (#4748)
On macOS a trackpad pinch-to-zoom is delivered as a rapid stream of ctrl+wheel events. The zoomed image kept its 0.05s transform transition during that stream, so each event restarted the in-flight transition from its interpolated mid-point and the image lagged and flickered. This is the same root cause as the #4451 pan flicker, which was fixed by dropping the transition during the gesture for the pan and touch-pinch paths; the wheel-zoom path was the only continuous gesture left with the transition on. Suppress the transition while a wheel-zoom gesture is streaming, cleared on a short debounce since wheel has no explicit gesture-end event. Discrete zoom (buttons, double-click, keyboard) keeps its smoothing. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6301c620a8 |
fix(library): import books opened via "Open with" by default on mobile (#4746) (#4747)
A recent change (#4407) made Android's "Open with Readest" (VIEW intent, used by Telegram and similar apps) always open the file as a transient book: into the reader but never written to the library, with its filePath pointing at the original content:// URI. Once that temporary URI grant dies the book can no longer be reopened, so the user has to re-share it from the source app every time. Make the transient behavior an opt-out gated by the existing autoImportBooksOnOpen setting, and surface it on mobile: - The VIEW handler now consults the setting via a new shouldOpenTransient predicate. When auto-import is on it falls through to the same library ingest path as a share-sheet SEND (full ingest plus cloud upload); when off it keeps the transient open. - Read the setting from disk in the handler rather than the settings store, since on a cold-start "Open with" the store is not hydrated yet and would wrongly fall back to a transient open. - Show the "Auto Import on File Open" toggle on mobile (was desktop only). - Default autoImportBooksOnOpen to true on mobile so shared files persist and sync by default; the desktop default is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
acd4a67dcf |
fix(reader): require a still-hold before instant-highlight on touch (#4745)
* fix(reader): require a still-hold before instant-highlight on touch Instant Highlight (the highlighter quick action) engaged on every pointer-down over text, calling preventDefault, which swallowed the single tap / swipe that turns the page on Android. Tapping the side margins still worked only because they are not selectable text; the synthetic-click fallback was also dead on Android (native touchend calls handlePointerUp with no event). Gate engagement behind a 300ms still hold for touch/pen: a tap releases first and a swipe moves first, so both fall through to pagination, and only a deliberate still hold starts drag-to-highlight. Mouse input keeps engaging immediately (click vs. press-drag is already unambiguous). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(agent): update agent memories Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b1346bf16d |
feat(wordlens): en-en glosses, styling, derivation lemmas, display-time cap (#4744)
* feat(wordlens): support en-en monolingual glosses Gloss difficult English words with a short English definition for learners reading English with English hints. - build: buildEnEn + shortDefGloss read ECDICT's English `definition` column (first/primary WordNet sense, POS-stripped, drop ;-example, <=24 word-boundary with trailing-connector trim). New `en-en` CLI branch; buildEnZh/buildEnEn now share a buildEnPack core. - gating: drop the hardcoded `hint === source` rejections (wordlensSection, WordLensPanel) so same-language packs are allowed; availability is decided by the manifest (resolvePack returns null when no pack exists). - data: data/wordlens/en-en.json (26,578 entries) + regenerated manifest.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(wordlens): gloss styling, derivation lemmas, display-time cap Builds on the en-en monolingual gloss support with refinements and regenerated packs. - settings: per-book gloss <rt> font size (em) and color in Settings > Language > Word Lens (getRubyStyles reads viewSettings). - en-en hints: WordNet hybrid (a simpler synonym, else a category hypernym, else the ECDICT definition) instead of raw verbose definitions. - lemmatization: gate difficulty by the lemma rank for every English source pair. enBaseFormCandidates now also covers -able/-ible suffixes and negative prefixes (un/in/im/ir/il), so insufferable resolves to suffer. A candidate is accepted when the English definition names the base OR the Chinese translations share a content character, which keeps true derivations (insufferable -> suffer) and rejects coincidental stems (capable -> cap). en-X packs inherit the en-en lemma table. - display cap: the max gloss length is applied at render time in cleanGloss (MAX_GLOSS_LEN), so the packs store the full hint and the cap can change without regenerating data. - tooling: pnpm wordlens:preview to sample pack entries; cache build corpora under data/wordlens/.sources (gitignored). - data: regenerate en-en, en-zh and en-de/es/fr/pt/ru plus the manifest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- 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> |
||
|
|
e982af1725 |
feat(reader): adjust text selection with Shift/Ctrl/Opt+Arrow keys (#4728) (#4738)
Support standard desktop shortcuts for refining an active text selection: Shift+Left/Right by character, Ctrl/Option+Shift+Left/Right by word. Only active while text is selected; otherwise the keys fall through to page navigation as before. Root cause: after a selection the reader container (not the book iframe) holds focus, so Shift+Left/Right keystrokes reach the parent shortcut handler and matched the page-turn shortcuts, turning the page instead of refining the selection. The new onAdjustTextSelection action runs before the navigation actions: when a selection is active it extends the iframe selection via Selection.modify() and suppresses the page turn; an iframe-forwarded key (already extended natively) just suppresses navigation. handleSelectionchange now refreshes the popup/range for keyboard-driven changes (no pointer drag) so the selection toolbar follows the refined selection. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
787641b5b1 | chore(agent): update agent memories (#4737) | ||
|
|
664b6125a2 |
feat(android): add monochrome themed launcher icon (#4733) (#4736)
Android 13+ recolors the adaptive icon's `<monochrome>` layer with a wallpaper-derived tint when the user enables themed icons. Support was added in #2122/#2153 (the `ic_launcher_monochrome.png` assets) but #2353 ("fixed launcher icon size") rewrote the committed adaptive icon to inset the foreground 22% and silently dropped the `<monochrome>` layer, so themed icons stopped working in shipped builds. Restore it by re-adding a `<monochrome>` layer (same 22% inset as the foreground) and shipping the monochrome mipmaps. The CI/release flow regenerates `gen/android` (`tauri android init` + `tauri icon`) then `git checkout .` to restore tracked customizations; `tauri icon` does not emit a monochrome layer, so the mipmaps are force-committed under `gen/` like the other customized resources. The monochrome artwork is redesigned: Android tints the layer via SRC_IN (alpha only), which flattened the old desaturated-logo asset into a solid blob. A narrow vertical center gap now splits the open book into two pages with a visible spine while keeping the bookmark, so the mark keeps its character when themed. Verified on a Pixel 9 Pro emulator (Android 36) with Themed Icons enabled, and guarded by src/__tests__/android/themed-icon.test.ts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bc9b8b23e6 |
fix(reader): stop per-chapter listener leak that degrades paragraph mode (#4735)
The annotator's foliate `load` handler (onLoad) attached a renderer `scroll` listener and, on Android, a global `native-touch` dispatcher listener on every section load. Both the renderer and the eventDispatcher outlive individual sections — and foliate fires `load` for preloaded neighbour sections too — so these listeners accumulated without bound, one set per chapter. Each renderer `scroll` (fired on every paragraph-mode `goTo`) then ran all of them, and on Android the scroll/native-touch handlers do real work. Reading a long book (e.g. a 3000-chapter web novel) in paragraph mode slowed down steadily after a few chapters and only an app restart cleared it. Register these listeners once per view via a new `useRendererInputListeners` hook with cleanup, instead of once per section load. The native-touch handler now resolves the CURRENT primary section's doc/index at fire time rather than capturing a (possibly off-screen, preloaded) section's. The redundant `scroll` → `repositionPopups` listener is dropped — a dedicated effect already repositions popups on scroll. Doc-scoped listeners stay in onLoad, since they die with the section's iframe. Add useRendererInputListeners unit tests covering register-once-per-view, no-accumulation-across-re-renders, latest-handler routing, Android gating, and unmount cleanup. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
140b71ee30 |
feat(dictionary): add adjustable dictionary popup font size (#4443) (#4734)
Expose `::part(dict-content)` on the MDict shadow content and add a dictionary popup font-size setting (Settings → Language → Dictionaries), independent of the main reading view. - mdictProvider: tag the in-shadow body with `part="dict-content"` and a stable `dict-shadow-host` class so the popup's `::part()` rule can reach across the shadow boundary — MDict is the only provider that renders into a shadow root, so ordinary popup CSS can't touch it. - DictionarySettings.fontScale (default 1) with setFontScale + load-merge; synced cross-device via the `dictionarySettings.fontScale` whitelist entry. - DictionaryResultsView drives `--dict-font-scale` + `data-dict-content` on each per-tab container. globals.css re-bases the light-DOM Tailwind text utilities to `em` within that scope and sizes the MDict shadow body via `::part(dict-content)`, so every provider scales from one lever. - SettingsSelect control (85–175%) in the Dictionaries panel. Tests: jsdom unit tests (part attribute, store fontScale, sync whitelist) plus a real-Chromium browser test for the em-rebasing + `::part` + custom- property-inheritance CSS contract jsdom cannot model. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
082edc204b |
fix(sync): sync updated book covers across devices (#4544) (#4731)
* docs: design for syncing updated book data (cover + file) (#4544) Cover-change sync via a content hash (coverHash = partial MD5 of cover.png) plus a cover_updated_at field-level merge timestamp; file updates ride the existing re-import / metaHash dedupe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): sync updated book covers across devices (#4544) Editing a book's cover wrote cover.png locally but changed no hash (the cover is keyed by the file hash), so peers had no signal to re-download it and the change never propagated. Give the cover its own content-addressed version: - coverHash = partial MD5 of cover.png; a peer re-downloads the cover iff the synced hash differs from the local one (idempotent, no churn on identical/re-extracted covers — compatible with the metaHash dedupe). - coverUpdatedAt = field-level LWW timestamp so a page-turn that wins whole-row LWW on updated_at can't clobber a cover edit (mirrors the reading_status_updated_at fix for #4634). Editing a cover recomputes the hash, bumps coverUpdatedAt, and re-uploads only the cover; the server merges cover fields independently; peers re-download on a hash diff. File updates continue to ride the existing re-import / metaHash dedupe (changed file -> changed hash -> re-key). Migration 016 adds cover_hash / cover_updated_at to books. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- 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> |