Commit Graph

2023 Commits

Author SHA1 Message Date
Huang Xin cead0f42e0 compat(css): fixed table layout and style in dark mode (#4055)
Closes #4028
Closes #4029
2026-05-04 17:36:03 +02:00
Huang Xin c59097b0ac feat(koplugin): add i18n catalog and sync info dialog (#4050)
- i18n loader at apps/readest.koplugin/i18n.lua: isolated callable,
  falls back to KOReader's gettext when a string is untranslated
- Translation catalog at locales/<i18next-code>/translation.po for 31
  languages, mirroring apps/readest-app/public/locales/
- scripts/extract-i18n.js (scan _("...") and _([[...]]); preserve
  existing, drop obsolete, add new) and scripts/apply-translations.js
  (bulk import from /tmp/koplugin-translations/<lang>.json)
- Mirror apps/readest-app SyncInfoDialog: rename showMetaHashInfo to
  showSyncInfo, dialog title "Sync Info", new Last Synced row computed
  as max(last_synced_at_config, last_synced_at_notes) from doc_settings
- syncconfig.lua / syncannotations.lua mark per-book sync timestamps
  on push/pull success
- Rename "Meta Hash" -> "Book Fingerprint" in koplugin and
  apps/readest-app SyncInfoDialog.tsx; translations propagated to
  all readest-app locales
- "book config" -> "reading progress" wording across user-facing
  strings (matches QiuYukang fork terminology)
- Replace "Log out as " / "Login failed: " concat prefixes with
  T(_("...%1..."), arg) placeholder pattern (RTL / verb-final friendly)
- pnpm lint:lua: luajit -b syntax check across koplugin .lua files;
  soft-skips when luajit is missing locally; CI installs luajit and
  runs the check unconditionally

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 05:35:36 +02:00
Huang Xin 7bb1133706 feat(dictionaries): add DICT/Slob formats and Web Search providers (#4048)
Extends the dictionary system beyond StarDict/MDict with two more open
formats and a pluggable Web Search tier so users can fall back to online
sources when their offline bundles miss a word.

Formats:
- DICT (dictd, RFC 2229): .index + .dict.dz bundles. Shared DictZip
  parsing with StarDict via new dictZip.ts helper.
- Slob (Aard 2): self-contained .slob containers, zlib-compressed
  utf-8 entries; non-zlib/non-utf-8 bundles flagged unsupported at
  import.

Web Search:
- Built-in templates for Google, Urban Dictionary, Merriam-Webster
  (seeded into providerOrder, disabled by default).
- Custom URL templates via %WORD% placeholder, URL-encoded at
  substitution; entries persist in settings.webSearches.
- V1 renders an "Open in {{name}}" external link (iframe embedding is
  blocked by every major target site's X-Frame-Options).

UI:
- CustomDictionaries panel: flat outline-primary buttons for Import /
  Add Web Search, end-aligned type badges for a uniform column,
  hover states, compact tips block.
- Dictionary popup: bottom-right Manage icon (tooltip-only) deep-links
  into Settings → Language → Dictionaries; rounded-corner clipping fix
  on the tab strip.

File picker accepts .index and .slob; importer recognizes DICT and
Slob bundles and reads bundle metadata for friendly names.

Tests cover DICT/Slob readers and providers with real freedict-eng-nld
fixtures, web search substitution + provider rendering, and the new
store CRUD for web searches.

Closes #4038
2026-05-03 20:07:27 +02:00
Huang Xin 8e7b2192d5 fix(reader): dismiss annotation popup on section info / progress bar tap (#4047)
Tapping the section info or progress bar overlays did not dismiss an
open annotation popup because the dismiss flow listens for
iframe-single-click, which only fires from inside the foliate iframe.
These overlays sit above the iframe and intercept taps before the
iframe sees them, so the popup stayed open.

Dispatch iframe-single-click first in their click handlers; if a popup
consumes it (existing useTextSelector handler dismisses popup/selection
and returns true), skip the original action.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:43:22 +02:00
Huang Xin e7564ffc27 fix(docker): correct Kong gateway port for client build arg (#4046)
The web client's NEXT_PUBLIC_SUPABASE_URL build arg pointed at port 7000,
but Kong is exposed on KONG_HTTP_PORT (8000 by default), so browser-side
Supabase calls failed in the self-hosted Docker setup.

Closes #4035

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:10:16 +02:00
Huang Xin e0fa6230ab feat(koplugin): support deletePluginSettings hook (#4045)
Implements the hook introduced by koreader/koreader#15240, called when
the user deletes the plugin via the plugin manager with "Also delete
plugin settings" checked. Removes the readest_sync entry from
G_reader_settings (auth tokens, user info, auto_sync flag, last_sync_at)
and resets the in-memory copy to defaults.

Closes #4039

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:31:02 +02:00
Huang Xin f5657fb3a0 fix(share): correct recipient import flow and assorted UI polish (#4043)
- ensureSharedBookLocal helper makes sure the local library has both the
  Book entry and the bytes on disk after /import succeeds; navigating
  into the reader before this lands on "Book not found"
- ShareLanding navigates via navigateToReader (path form on web) so the
  reader actually renders instead of hitting the App Router stub and
  going blank
- Loading + progress UI on the landing page while bytes stream in;
  Open-in-app disabled mid-import to avoid races
- UserInfo header: vertically center avatar with name/email, tighter
  mobile gap, and a fillContainer prop on UserAvatar so a parent can
  size the box via classes without the inline style fighting back
- Rename "Share current page" -> "Share reading progress" (and matching
  post-generation hint) and shrink the dialog from 480 to 460px
- Drop the unused Reload Page menu item from SettingsMenu
- Translate the two new i18n keys across 31 locales

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:16:46 +02:00
Huang Xin 19f2414f42 fix(share): hide download link on share landing page (#4041)
Direct file download from the public share landing carries rights /
abuse risk. Replace the Download button with the Open-in-app deep link
in both the logged-in flow (now: Add to library + Open in app) and the
anonymous flow (now: Open in app + Get Readest footnote).

The /api/share/[token]/download route is left intact so re-enabling
the button later is a one-file UI change.
2026-05-03 06:06:11 +02:00
Huang Xin 19f3e65b62 fix(share): make /s landing build under Next 16 layout-prop validation (#4040)
* fix(share): make /s landing build under Next 16 layout-prop validation

Production builds (`next build --webpack` for OpenNext on Cloudflare)
rejected the Layout default export with "Type 'LayoutProps' is not valid"
because Next 16 strictly enforces that layout components only accept
`{ children }` (and `{ params }` for dynamic segments) — never
`searchParams`. The previous design tried to read `searchParams` from
both the layout component AND its `generateMetadata`, but layouts don't
get `searchParams` at all (they're shared across child pages with
potentially different query strings).

Restructure:
- Move `generateMetadata` from `app/s/layout.tsx` to `app/s/page.tsx`,
  which DOES receive `searchParams`. Page is now a server component.
- Split the existing client component into `app/s/ShareLanding.tsx`
  (still `'use client'`); page.tsx wraps it in `<Suspense>` per the
  Next 16 client-component contract.
- Delete `app/s/layout.tsx` — no longer needed; the project's root
  layout still wraps everything.

Verified locally: `pnpm lint` clean, `pnpm build-web` green, all 9
share API routes plus `/s` show up in the route manifest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(share): drop edge runtime from og.png so OpenNext can bundle it

OpenNext on Cloudflare errors out when bundling edge-runtime routes inside
the default server function:

  app/api/share/[token]/og.png/route cannot use the edge runtime.
  OpenNext requires edge runtime function to be defined in a separate
  function.

Splitting into a second function bundle is more config surgery than this
route deserves. `next/og`'s `ImageResponse` (Satori + WASM yoga/resvg) has
supported the default Node-compat runtime since Next 13.4, and on
Cloudflare via OpenNext the default function IS already a Worker, so
cold-start cost is similar to edge.

Verified: `pnpm exec opennextjs-cloudflare build` now completes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 04:40:16 +02:00
Huang Xin d1e7b4902c feat(share): time-limited share links with cfi-aware imports (#4037)
Add a Share Book feature that generates an expiring HTTPS share URL plus a
parallel readest://share/{token} deep link. Recipients land on /s/{token},
where logged-in users can one-tap "Add to my library" (R2 server-side
byte-copy) and anonymous users download the book or open it in the app.
Sharers manage active links from a dedicated "Manage Shared Links" panel
under user settings.

Highlights:
- 9 new App Router endpoints under /api/share (create, [token], cover,
  og.png, download, download/confirm, import, revoke, list).
- /s landing page with branded next/og chat unfurl image and SSR auth-cookie
  detection so logged-in recipients see "Add to my library" as the primary
  action without layout shift.
- Server-side R2 byte-copy for /import preserves the project's existing
  invariant that every files.file_key is prefixed with its row's user_id;
  stats / purge / delete / download routes work unchanged. URL-encodes the
  copy source so titles with spaces or '&' don't break the copy.
- Universal 7-day expiry cap, no tier differentiation, no "never". DMCA-risk
  reduction. Picker defaults to 3 days.
- Position-aware shares: "Share current page" toggle (off by default for
  privacy) attaches the sharer's CFI; recipient lands at the same paragraph.
- Per-user 50-share cap, rate limiting via Cache-Control: no-store on
  token-bearing responses, atomic SQL increment for download_count via a
  SECURITY DEFINER function so the public confirm beacon stays safe under
  concurrent fire.
- Soft revocation: presigned download URLs (5-min TTL) cannot be cancelled
  before TTL; documented as accepted v1 behavior.
- token + token_hash hybrid storage: public endpoints look up by hash and
  never select the raw token, so accidental SELECT-* leakage on a public
  route can't expose the bearer credential.
- Mobile / desktop Tauri share via tauri-plugin-sharekit; web falls back to
  navigator.share with a clipboard fallback when no native share method
  exists. Share-sheet dismissal no longer silently copies.

UI:
- New Dialog with a settings-card group: iOS-style segmented duration picker
  + toggle slider for "Share current page", on a single row each.
- Reader top-bar Share button, library context-menu Share entry, manage-
  shares list with cover thumbnails and overflow menu.
- New <SegmentedControl> primitive in src/components for reuse.

Coverage:
- Unit tests for token utils + URL parser (20 new tests, full suite at 3445).
- 31 locales translated for all new strings; en plurals hand-added per the
  project's hand-curated en convention.

DB migration in docker/volumes/db/migrations/002_add_book_shares.sql adds
the book_shares table, RLS policies, and the increment_book_share_download
RPC. Migration is idempotent.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:03:35 +02:00
Huang Xin 176e5df771 refactor(settings): move Keep Screen Awake to Behavior > Device (#4027)
Relocate the toggle from the library settings menu to the Behavior settings
panel under the Device section. Add a matching command palette entry so the
setting remains discoverable via search.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:44:41 +02:00
Huang Xin 579e950756 fix(rsvp): split em-dash and en-dash compound words (#4026)
Speed-read mode flashed words joined by em-dash (—) or en-dash (–) as
a single unreadable token. Split non-CJK tokens on these dashes,
keeping the dash attached to the preceding word so the punctuation
pause still fires, and treat them as pause-triggering punctuation in
the word display duration.

Closes #4022

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:26:03 +02:00
Huang Xin eadb355396 fix(txt): recognize 番外/外传 chapter prefixes, closes #4016 (#4025)
Chinese novels commonly use 番外 (bonus), 番外篇, or 外传 as chapter
headings, optionally combined with 第N章. The previous regex only
matched 第N章 at line start, so lines like "番外 第1章 旗开得胜"
were dropped from the TOC. Treat 番外篇/番外/外传 as preface-style
keywords so they match alongside 楔子/前言/etc.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:13:28 +02:00
Huang Xin 8ba052dc81 fix(reader): pure black/white footer in eink mode (#3873) (#4024)
The footer NavigationBar and slide-up panels (Color, Progress, Font &
Layout) used `bg-base-200`, which is computed as a slightly-darker shade
of the theme's background. In eink mode this rendered as a visible grey
strip even when the user picked a pure white/black theme.

Switch to `bg-base-100` in eink mode and add a `border-base-content` top
border so the bar stays visually separated from the page area without
relying on a tinted background, matching the existing eink treatment
elsewhere (e.g. PageNavigationButtons, ImageViewer).
2026-05-01 18:05:36 +02:00
Lex Moulton 293d5b5f5d fix(rsvp): cross-device resume seeding + mobile slider drag (#4004)
* fix(rsvp): seed local position from synced BookConfig on resume

* refactor(rsvp): simplify seedPosition

Consolidate the matched/mismatched write paths into one localStorage
write, extract stripCfiPath() to a module-level helper, and trim the
comments around it.

* fix(rsvp): make progress bar draggable on mobile

Three coordinated fixes so the RSVP overlay's seek bar works reliably
under touch:

- Add `touch-action: none` on the slider so the mobile browser stops
  claiming the gesture for scroll/pan and firing pointercancel mid-drag.
- Hoist the `.rsvp-controls`/`.rsvp-header` exclusion to the top of the
  overlay's touchend handler so a successful drag isn't immediately
  re-interpreted as a speed-change swipe.
- Guard `releasePointerCapture` with `hasPointerCapture` so pointercancel
  arriving after the browser has already released capture (multitouch,
  app backgrounding) no longer throws NotFoundError.
2026-05-01 17:42:58 +02:00
Huang Xin fb37406b31 feat(annotations): preview mode for deep-link landings (#4019)
* feat(annotations): preview mode for deep-link landings

When the reader opens at a deep-link CFI (e.g. clicking an exported
highlight from Obsidian), the position should not be persisted as the
user's reading progress until they actually start reading. Otherwise
the deep-link visit overwrites their last-read position and propagates
that across all sync targets.

Adds a per-book `previewMode` flag in the reader store that:

- Is set to true in FoliateViewer when the URL's `?cfi=` overrides the
  saved last-position.
- Is cleared on the first user-initiated relocate (page turn / scroll),
  reusing the existing reason filter in `docRelocateHandler`.
- Gates the auto progress writers:
    - useProgressAutoSave — skip local config persist
    - useProgressSync     — skip auto-push and skip the remote-progress
                            view.goTo (so cloud pull doesn't yank the
                            user away from the previewed annotation)
    - useKOSync           — skip auto-push (manual pushes still respected)

Hardcover sync and Discord presence are unaffected: hardcover only
fires on explicit user button press, and Discord presence carries no
position information.

Also picks up the regenerated AndroidManifest.xml change from the
existing tauri.conf.json deep-link config (registers readest:// scheme
on Android so the smart landing page's intent:// launch resolves).

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

* fix(annotations): jump in place when target book is already open

When an annotation deep link arrives while the user is already in the
reader (most common case on mobile App Links), navigateToReader was
pushing the same /reader path with a different cfi query param. The
reader's init useEffect has [] deps, so it doesn't re-run, and
FoliateViewer doesn't re-read the cfi — the view stayed put.

Detect a mounted view for the target book hash by walking
viewStates and matching the hash prefix on the bookKey. If found,
call view.goTo(cfi) directly and set previewMode so the existing
gates fire. Falls back to navigateToReader when no view is open.

Also adds a console.log on each parsed deep link to make this path
easier to debug from device logs in the future.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:37:50 +02:00
Huang Xin 486659a1ca feat(annotations): deep links for highlight exports (#4018)
* feat(annotations): deep links for highlight exports

Embed an HTTPS deep link in markdown export so clicking a highlight in
Obsidian / Notion / Mail launches Readest at the exact CFI position.
Mobile App Links / Universal Links open the native app silently when
installed; desktop attempts the readest:// scheme automatically with a
manual fallback.

- Markdown export wraps the page-number text in a per-annotation link:
  https://web.readest.com/o/book/{hash}/annotation/{id}?cfi=...
- New /o/... smart landing page handles platform routing (intent:// on
  Android Chrome, scheme + visibility-cancel on other Android, auto
  scheme + 1 s fallback on desktop, manual button on iOS).
- Reader honors a ?cfi= query param on initial load (overrides the
  saved last-position for the primary book only).
- New useOpenAnnotationLink hook handles incoming readest:// and
  https://web.readest.com/o/... URLs, including cold-start (getCurrent)
  and library-load deferral; supports the legacy flat shape
  readest://annotation/{hash}/{id} from previous Readwise syncs.
- ReadwiseClient now emits the HTTPS deep link instead of the legacy
  custom scheme.
- AASA extended with /o/* matcher; Android intent-filter for the host
  has no pathPrefix so it already covers it.

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

* i18n: translate annotation deep-link strings across all locales

Translates the 13 new keys introduced for the annotation deep-link
feature into all 31 supported locales. Replaces all 403
__STRING_NOT_TRANSLATED__ placeholders.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:26:47 +02:00
Huang Xin 5a0a70a30a feat(reader): custom dictionaries (StarDict + MDict) (#4012)
* feat(reader): custom dictionaries (StarDict + MDict)

Adds a pluggable dictionary provider system. Built-in Wiktionary +
Wikipedia (extracted from the legacy single-popup model into a tabbed
shell) plus user-importable StarDict (.ifo/.idx/.dict.dz/.syn) and MDict
(.mdx/.mdd) bundles.

Settings → Language → Dictionaries: import / enable / drag-reorder /
delete (delete-mode toggle mirrors CustomFonts). Drag uses @dnd-kit with
pointer/touch/keyboard sensors. Reader popup: tabbed UI, per-tab lookup
history, scroll-aware back button, last-active tab persists. Tabs grow
to natural width up to a cap, truncate with ellipsis when crowded; phantom
bold layer prevents layout shift on focus.

StarDict reader is self-contained (replaces unused foliate-js/dict.js),
with lazy random-access binary search on .idx + .syn (~420 KB Int32Array
of byte offsets vs ~10 MB of parsed JS objects), lazy DictZip chunk
decompression via fflate streaming Inflate (cmudict/eng-nld both
chunked), and an optional .idx.offsets sidecar generated at import to
skip the init scan. Cmudict 105K-entry init drops from ~10 MB heap and
2 MB IO to ~1.7 MB heap and ~500 KB IO.

MDict uses the readest/js-mdict fork (added as a submodule, consumed via
tsconfig paths so deps stay out of readest's pnpm-lock) which adds a
browser-friendly BlobScanner reading via blob.slice(...).arrayBuffer()
— slices are lazy when the Blob is Readest's NativeFile / RemoteFile.
encrypt=2 (key-info-only) MDX is fully supported via ripemd128-based
mdxDecrypt; encrypt=1 (record-block, needs user passcode) surfaces as
unsupported.

Wikipedia annotation tool removed (Wikipedia is now a tab inside the
unified popup); legacy WiktionaryPopup / WikipediaPopup deleted. Stale
annotationQuickAction === 'wikipedia' coerced to 'dictionary' on settings
load. iOS-friendly external links: skip target="_blank" on Tauri to
avoid the WebView's "open externally" path triggering the shell scope
error; the popup's container click handler routes through openUrl.

i18n: 939 strings translated across 31 locales (30 base keys + CLDR
plural forms for ar/he/sl/pl/ru/uk/ro/it/pt/fr/es).

Test fixtures bundled: cmudict (StarDict, 105K entries), eng-nld
(StarDict, smaller), and a Longman Phrasal Verbs MDX (encrypt=2).
3396 unit tests pass.

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

* fix(stardict): resolve fflate from js-mdict source for vitest + Next

js-mdict is consumed as TypeScript source via tsconfig paths from
packages/js-mdict/src/. Its sources `import 'fflate'` directly, but
fflate is only installed under apps/readest-app/node_modules — so
vite's import-analysis (and Next/Turbopack's resolver) can't find
fflate when it walks up from the redirected js-mdict source location.
CI's fresh checkout exposes this; locally a leftover
packages/js-mdict/node_modules/fflate from the old workspace setup
masked it.

Pin fflate resolution to apps/readest-app/node_modules/fflate in:
- vitest.config.mts (Vite alias)
- next.config.mjs (webpack alias + Turbopack resolveAlias — Turbopack
  rejects absolute paths so use a project-relative form)
- tsconfig.json (paths entry so tsgo / Biome see it)

Verified by deleting packages/js-mdict/node_modules locally and
re-running pnpm test (3396 pass), pnpm lint (clean), and both
pnpm build-web and a tauri-platform Next build (clean).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:16:36 +02:00
dependabot[bot] a9684ab357 chore(deps): bump amondnet/vercel-action in the github-actions group (#4008)
Bumps the github-actions group with 1 update: [amondnet/vercel-action](https://github.com/amondnet/vercel-action).


Updates `amondnet/vercel-action` from 25 to 42
- [Release notes](https://github.com/amondnet/vercel-action/releases)
- [Changelog](https://github.com/amondnet/vercel-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/amondnet/vercel-action/compare/v25...v42)

---
updated-dependencies:
- dependency-name: amondnet/vercel-action
  dependency-version: '42'
  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>
2026-04-30 07:03:44 +02:00
Huang Xin a43845b4c5 fix(layout): symmetric margins and gap in 2-column layout, closes #3909 (#4002) 2026-04-29 20:30:48 +02:00
Huang Xin 1d8ed3fc92 fix(footnote): ignore background image in footnotes (#3998) 2026-04-29 18:01:23 +02:00
Huang Xin d609de58f0 fix(reader): preserve position when toggling scrolled mode, closes #3987 (#3996)
The paginator's scrolled-mode scroll handler is debounced 250 ms, so
#anchor and #primaryIndex can lag behind the user's actual viewport.
Toggling out of scrolled mode within that window made
render() → scrollToAnchor(#anchor) restore the stale anchor, reverting
the position to a previously visible section.

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

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

Add regression coverage for the scroll-anchor restore path.

Closes #3876.
2026-04-28 20:26:55 +02:00
Huang Xin ad55375f89 fix(tts): preserve state on AbortError to keep rate changes effective on iOS, closes #3949 (#3988)
iOS WKWebView's in-flight audio.play() rejects with AbortError after audio.src is reset
during a stop+restart cycle. That rejection leaks through one of the .catch chains into
TTSController.error(), which unconditionally flipped state to 'stopped' — desyncing the
state machine so subsequent rate changes fell into the no-op else branch and #speak's
auto-forward gate stopped firing at paragraph end.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:45:05 +02:00
Huang Xin 34f19fd148 fix(annotation): preserve line breaks in selected text across <br> elements, closes #3981 (#3986)
In multi-line PDF selections, pdf.js renders each text run as its own <span>
and inserts <br role="presentation"> at line endings. getTextFromRange only
walked text nodes, so <br>s were dropped and adjacent line-final/line-initial
words glued together (e.g. "lastfirst") in highlights, notes, and AI inputs.

Walk elements alongside text nodes and emit "\n" for <br>, mirroring how
Selection.toString() handles line breaks.
2026-04-28 18:56:55 +02:00
Huang Xin 920627ae59 feat(rsvp): use jieba tokenizer to segment words for Chinese books (#3985) 2026-04-28 18:19:05 +02:00
Huang Xin 4b0720a3e3 perf(rsvp): windowed context, extraction caching and lazy CFI for sections with thousands of words, closes #3953 (#3984)
* perf(rsvp): windowed context, extraction caching and lazy CFI for sections with thousands of words, closes #3953

* i18n: update translations
2026-04-28 17:29:33 +02:00
Huang Xin 3b03b2c8d5 fix(txt): more robust txt parsing, closes #3970 (#3983) 2026-04-28 15:35:35 +02:00
Huang Xin 6fbf9ef68b fix(layout): fixed layout for catalog title (#3982) 2026-04-28 15:34:29 +02:00
Zeedif ca8f0fe9f6 feat(opds): add OPDS-PSE streaming support and custom OPDS 2.0 parser (#3951)
* feat(opds): add OPDS-PSE streaming support and custom OPDS 2.0 parser

* refactor(opds): remove custom parser, use updated foliate-js dependency

* fix(opds): resolve PSE auth at fetch time, drop credentials from book.url

Previously the streaming `pse://` virtual file baked the proxy URL with
the basic auth header into `book.url`, which (a) failed on desktop where
no proxy is used because the auth header was never applied to the page
fetch, and (b) leaked the credential to the sync server because transient
books still get pushed on first sync.

Now the `pse://` payload stores only the upstream OPDS template URL plus
the catalog id. A new `createPseStreamPageLoader` looks up the catalog
from settings on each open, probes auth once (cached for the session),
and applies the auth header via `tauriFetch` on desktop or via the proxy
URL on web.

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

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:14:44 +02:00
Lex Moulton 6d5e59c79a fix(rsvp): resume at stop word, prevent section replay, restore full context (#3960) 2026-04-28 09:02:40 +08:00
Huang Xin 6fcda66b60 fix(reader): close stuck reader window on book load failure, closes #3932 (#3980)
When a book's underlying file is missing, opening it in a dedicated
reader window showed an error toast then navigated that window to
/library, leaving a leftover library-in-reader-window the user had
to close manually. Route the recovery through a new
closeReaderWindowOrGoToLibrary() that closes the dedicated reader
window (after ensuring the main library window is visible) and only
falls back to /library navigation in the main window or on web.

Also fix a related macOS issue: the reader's CloseRequested handler
was running handleCloseBooks and calling currentWindow.destroy() on
the main window, which tore down the active book and bypassed the
Rust close-to-hide handler — making Cmd+W / traffic-light close
quit the app from the reader page (vs. correctly hiding from the
library page) and lose the active book even when the window did
hide. Skip both the cleanup and destroy on macOS for the main
window so the Rust handler hides it with the book intact, matching
the macOS minimize-to-dock convention.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:32:04 +02:00
Huang Xin 94ede10f6e fix(annotator): defer Android quick action until touchend, closes #3935 (#3979)
On Android, long-press selects text via selectionchange while the finger
is still on the screen. The quick action handler was gated by
androidTouchEndRef and silently returned, so no popup ever opened. After
the user lifted, nothing re-ran the gated action.

Track the gated action in a small DeferredActionState ref and flush it
from the native touchend handler, so instant copy/dictionary/wikipedia/
search/translate/tts now fire on the first long-press release.
2026-04-27 17:37:22 +02:00
Huang Xin 4f55920b71 feat(kosync): add metadata hash info dialog to diagnose sync failures (#3978) 2026-04-27 17:04:29 +02:00
Huang Xin 63b0b87028 fix(layout): fixed dropdown menu layout for the delete button in details, closes #3940 (#3976) 2026-04-27 15:50:51 +02:00
Huang Xin 17f2a17adc fix(toc): fix auto scroll on book open with pinned sidebar, closes #3945 (#3975) 2026-04-27 15:35:19 +02:00
Huang Xin e18bfd6810 fix(reader): smooth out mouse wheel scrolling in scroll mode, closes #3966 (#3974)
The browser delivers one large quantised delta per wheel notch, which
Chromium scrolls without interpolation — producing the jerky one-step
motion reported on Windows. Detect mouse-wheel-shaped events inside
the iframe (line-mode, or single-axis with |deltaY| ≥ 50), suppress
the native scroll, and replay the delta as an rAF exponential lerp on
the renderer's container. Trackpad / high-resolution input is left to
native scrolling so its momentum and 2-axis behaviour are preserved.
2026-04-27 14:58:43 +02:00
Huang Xin 6d798542f6 fix: restore main library window when going to library from reader, closes #3969 (#3973)
When the main window has been destroyed (Windows/Linux default close), the
reader's "go to library" button only closed the reader, leaving no library
visible. Add ensureMainLibraryWindow() that shows an existing main window
or recreates one with the 'main' label so the existing close-reader-window
wiring keeps working. Also grant the cross-window show/unminimize permissions
the call now needs.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:22:28 +02:00
loveheaven ebbbf104b2 feat(cjk): support inline annotation(warichu, Gezhu) layout (#3934)
* feat(warichu): support warichu (割注) inline annotation layout

- Add warichu HTML transformer that converts <span class="warichu"> and
  <warichu> elements into .warichu-pending placeholders during content load
- Implement runtime layout (layoutWarichu / relayoutWarichu) that measures
  column position and splits text into small inline-block chunks (2 chars
  each) so CSS column boundaries can break between them, preventing large
  blank gaps in vertical-rl pagination
- Use column stride (column-width + column-gap) for accurate position
  measurement across column boundaries
- Hook into stabilized event for initial layout and relayout on resize
- Add warichu CSS styles (inline-block chunks, half-size font, vertical align)

* fix(warichu): correct HTML slicing edge cases

- sliceHtml: re-emit tags that were already open before the slice start
  so the result stays well-formed. Previously a slice past an opening
  tag produced an orphan closing tag (e.g. "<b>Hello</b>"[3,5] →
  "lo</b>" instead of "<b>lo</b>").
- sliceHtml / removeFirstVisibleChar / removeLastVisibleChar: treat HTML
  entities (e.g. &amp;) as one visible character so they aren't split or
  truncated mid-entity (e.g. removeFirstVisibleChar("&amp;rest") →
  "amp;rest").
- buildNodes: drop a duplicate chunk.appendChild(l2).
- Add unit tests covering the above for the three pure helpers.

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-04-27 19:12:50 +08:00
Huang Xin fa61f40262 chore: bump opennext and wrangler to the latest versions (#3971) 2026-04-27 10:56:59 +02:00
fabrisnick b251e537d3 chore(deps): update stripe (#3941)
* chore(deps): update stripe

* fix: filter and sort not affecting purchases

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* refactor: rename restoredPurchases to restoredSubscriptions

The filter excludes one-time purchases, so the array contains only
subscriptions. Naming reflects that.

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:24:00 +02:00
Huang Xin aa60123d38 fix(rsvp): unicode-aware ORP calculation for non-Latin scripts, closes #3958 (#3964)
JavaScript's `\w` only matches ASCII, so the ORP letter-count regex stripped
all characters from Cyrillic (and other non-Latin) words, producing length 0
and always highlighting the first letter. Use `\p{L}\p{N}_` with the `u` flag
so letters from any script count toward the word length.
2026-04-27 07:00:23 +02:00
JustADeer 1527dd9b3a fix: exponential wheel zoom for images and tables, closes #3956 (#3957)
* Fix ZoomLabel not showing during pinch zoom

* fix: exponential wheel zoom and show ZoomLabel during pinch

- Use exponential wheel zoom (Math.exp) instead of additive for smoother PC scroll
- Show ZoomLabel during pinch start and pinch move
- Clean up unused setZoomSpeed state for wheel handler
2026-04-27 06:17:38 +02:00
Huang Xin 38d7ba80fe feat(opds): support auto-download books from OPDS feeds (#3844)
* feat: auto-download new items from subscribed OPDS catalogs

Add opt-in auto-download per OPDS catalog. When enabled, new publications
are downloaded on app startup and pull-to-refresh. Dedup via Atom <id>.

- Extend foliate-js parser to surface entry id/updated fields
- Add subscription state to OPDSCatalog type
- New headless opdsSyncService with navigation feed crawling
- Auto-download toggle in CatalogManager UI
- Wire sync into startup and pull-to-refresh hooks
- 16 new test cases

Built with an AI code agent.

Closes #3836

* chore: point foliate-js submodule to fork with OPDS id/updated fields

CI needs to fetch the foliate-js commit that adds id and updated field
extraction from OPDS feeds. Point submodule URL to our fork where the
commit is pushed.

* feat(opds): rearchitect auto-download with two-phase OPDS sync

Replaces the monolithic opdsSyncService from #3837 with a separated
discovery + acquisition pipeline. Subscription state lives in
per-catalog JSON files under Data/opds-subscriptions/, decoupled from
catalog config.

Discovery (services/opds/feedChecker.ts) resolves a catalog URL down
to its "by newest" feed via three detection tiers — rel="http://opds-
spec.org/sort/new" (Calibre / Calibre-Web), title heuristics
("Newest", "Recently Added", "Latest"), and href patterns
(?sort_order=release_date for Project Gutenberg, /new-releases for
Standard Ebooks). It then walks only that feed plus rel=next
pagination, never the full navigation tree. When no by-newest feed is
found, the catalog is skipped with a warning instead of silently
mirroring everything.

Acquisition link selection filters to safe rels
(acquisition / acquisition/open-access — drops buy / borrow /
subscribe / sample / indirect), prefers open-access when both rels
are present, then ranks by format tier:
  0  Advanced EPUB / EPUB 3 (link title, .epub3 href, version=3.x)
  1  plain EPUB
  2  MOBI / AZW / AZW3
  3  PDF / CBZ
  4  anything else
Within a tier, ties resolve by feed order. When the upstream omits or
mis-declares (octet-stream) the link's media type, format is inferred
from href extension and link title. Entries that appear in both
feed.publications and a group are deduped within the same batch.

Orchestration (services/opds/autoDownload.ts) runs catalogs
sequentially — per-catalog concurrency already caps parallel downloads
at 3, so a parallel fan-out across catalogs would be N×3 simultaneous
requests on cellular. Pending and retry items are deduped by entryId,
and entries still inside their exponential-backoff window are skipped
instead of re-attempted (avoids appending duplicate failedEntries
records). Filenames come from the last path segment, capped at 200
chars, with a try/catch around decodeURIComponent for malformed
%-sequences.

Hook (hooks/useOPDSSubscriptions.ts): startup, 5-minute interval, and
'check-opds-subscriptions' event triggers. Toggling auto-download on
fires the event so the sync runs immediately. Newly imported books
are queued for cloud upload via transferManager when the user is
logged in and settings.autoUpload is on, mirroring the manual OPDS
download path. 'opds-sync-complete' is dispatched after every sync so
listening UI can refresh without polling. loadSubscriptionState
self-heals duplicate failedEntries from older state files.

The submodule URL is reverted to readest/foliate-js (the changes
needed for OPDS id/updated extraction are already in main), and
OPDSCatalog is trimmed to only the autoDownload field.

Verbose [OPDS]-prefixed debug logs cover the whole pipeline: feed
fetch → resolve → discovery → per-item download (with magic-byte
dump of the saved file) → import → cloud upload queueing.

34 new tests across opds-feed-checker, opds-auto-download,
opds-subscription-state, opds-types.

Closes #3836.
Supersedes #3837.

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

---------

Co-authored-by: Johannes Mauerer <johannes@mauerer.info>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 06:04:32 +02:00
Archisman Panigrahi 8a8fcb2611 chore: add VCS browser URL to appdata.xml (#3962)
This is now recommended in flatpak. See warning in https://github.com/flathub/com.bilingify.readest/pull/20#issuecomment-4322583486
2026-04-27 09:34:30 +08:00
Huang Xin d488f65447 fix(seo): fixed the title and description of the user page (#3952) 2026-04-25 09:56:13 +02:00
Huang Xin 528a13e36a fix(layout): don't dismiss notebook on navigate to annotations when notebook is pinned, closes #3923 (#3948) 2026-04-24 17:42:17 +02:00
Huang Xin 71371130e0 fix(android): avoid rsproperties panic on startup, closes #3922 (#3942)
`rsproperties::get` panics when it can't open or parse the
`/dev/__properties__` layout (documented behavior of the crate).
On some older/unusual Android builds (e.g. MediaTek Android 8.1 on
Xiaomi Mipad), this aborts the app with SIGABRT before the main
window is created.

Replace the crate with a direct FFI call to Android's native
`__system_property_get`, which has existed since the earliest
Android versions and returns an error code instead of panicking.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 06:00:31 +02:00