Builds the URL-clipping path of the "Send to Readest" feature: paste a
link, the renderer ingests the rendered page, and a self-contained EPUB
lands in the library. No server proxy, no external CDN refs left in the
EPUB once it's saved.
Architecture
- New Rust `clip_url` command spawns a hidden Tauri WebviewWindow at the
target URL with a real Chrome UA + WebKit fingerprint mask, so TLS-
fingerprint and JS-challenge walls (Cloudflare, Medium, X, WeChat MP)
resolve naturally instead of bouncing the server proxy.
- Capture transport is URL-payload navigation to a one-shot
127.0.0.1:RANDOM_PORT/clip/{token}?d={url-safe-base64} listener.
Top-level navigation isn't governed by CSP connect-src / form-action /
WebKit Private Network Access — the four earlier transports
(fetch, <form>, custom URI scheme, window.name) were each blocked by
one of those.
- Page-to-EPUB bundler (`assetBundler`) walks <img>/<picture> with
src → data-src → data-original → data-srcset → srcset fallback so lazy-
loading sites don't ship a 60px LQIP; fetches assets in parallel with a
per-asset timeout + per-asset/total caps; failed images degrade to alt-
text placeholders. A per-site rules table (seeded with WeChat MP) + a
selector fallback catches articles Readability misextracts. Builder
prepends the article <h1> + byline so the EPUB has a proper opening.
- Nested EPUB TOC built from h1–h6.
UI surfaces
- "From Web URL" entry in the library Import menu, gated to Tauri; web
build hides the URL field and points at the browser extension.
- `ImportFromUrlDialog` with auto-height (overrides Dialog's `sm:h-[65%]`
default) and a dim placeholder for the URL field.
- Clip webview window styled to match Readest's main window — macOS
decorations + overlay title bar; other desktops decorationless with a
drop shadow; native background + in-page loading overlay pick up the
caller's `themeCode.bg`/`fg` so light/dark/eink/custom themes all
render correctly. Title localised, all five overlay/title strings
translated across 33 locales.
Notes
- Gates the macOS traffic-light positioner to main/reader-* windows so
the decorationless clip window no longer null-derefs in
`position_traffic_lights`.
- Stricter validation across the path: schemes restricted to http/https,
hex-color parsing rejects malformed values, server endpoint returns
400 on missing/invalid base64.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(library): add Import from Folder dialog with format/size filters
Replaces the silent "import every supported file recursively" behaviour of the directory import menu item with an explicit dialog that lets users pick which formats to include, set a minimum file size, and choose between mirroring subfolders as nested groups (legacy behaviour) or flattening every match into the current library view.
The folder, the chosen Folder Structure radio, the ticked File Formats and the File Size threshold are all persisted in localStorage so re-opening the dialog seeds every field with the user's last choice. Cancelling the dialog does not write to storage so an aborted pick won't pollute the next session.
Also hides the native number-input spinner via a small .no-spinner utility in globals.css; on macOS WebKit the spin buttons were drawing over the rounded input border and looked broken. The KB suffix now lives inside the input's bordered shell instead of beside it.
Two correctness fixes the dialog flow exposed:
* The library importer + ingestService now treat groupId as a tri-state — undefined means "don't touch the existing group", '' means "explicitly the library root", any other string means a specific group. Previously a falsy check in both layers conflated '' with undefined, so re-importing a deduped book under flatten mode silently kept its stale groupId/groupName from the prior keep-as-groups run, making the book reappear in the old subfolder group instead of moving into the library root. New regression tests in ingest-service.test.ts cover both the empty-string case and the omitted case.
* Imports of arbitrary user paths (e.g. ~/Downloads) now go through a new allow_paths_in_scopes Tauri command that extends both fs_scope and asset_protocol_scope. The dialog plugin only auto-grants fs_scope, so reads through the asset protocol (RemoteFile / convertFileSrc) used to fail with "asset protocol not configured to allow the path". The shim is invoked after every selectFiles / selectDirectory call and once more at the start of runFolderImport so localStorage-restored paths are also covered. Granted scopes persist across restarts via tauri_plugin_persisted_scope.
* fixup(library): harden Import-from-Folder scope grant + RTL/dialog polish
Three review fixes on top of the Import-from-Folder feature:
* lib.rs: refuse to extend asset_protocol_scope for paths not already
in fs_scope. Without this gate, any frontend code (XSS via book
content, OPDS HTML, dictionary lookups, or a compromised dependency)
could call allow_paths_in_scopes with '/' or '~/.ssh' and gain
persistent read access to arbitrary user files via the asset
protocol — the grant survives restarts thanks to
tauri_plugin_persisted_scope. Mirrors the defensive check in
dir_scanner.rs.
* ImportFromFolderDialog.tsx: migrate from a custom ModalPortal chassis
to the project's shared <Dialog> primitive so eink mode auto-removes
shadows, mobile gets the bottom-sheet treatment, RTL direction is
applied, and focus management is correct.
* ImportFromFolderDialog.tsx: swap directional Tailwind utilities for
the logical equivalents (text-start, ps-/pe-, rounded-s-, text-end)
per DESIGN.md §2.8 — Arabic/Hebrew users were getting a mirrored
number-input row with the KB suffix on the wrong side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n(library): translate Import-from-Folder dialog strings across 33 locales
Translates the 13 new strings introduced with the Import-from-Folder
dialog (folder picker label, format-filter section, size-threshold
input, folder-structure radios, OK button, empty-result toast). All
33 supported locales — including RTL fa/he/ar — are now complete; no
__STRING_NOT_TRANSLATED__ placeholders remain in the catalog.
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>
Hand selected words off to the platform's native dictionary surface
when the user opts into the new "System Dictionary" entry under
Settings → Languages → Dictionaries. The setting is exclusive: enabling
it disables all other providers (and vice versa) so the in-app lookup
button either always opens the popup or always invokes the OS — no
mixed states.
Per platform:
- macOS: AppKit's -[NSView showDefinitionForAttributedString:atPoint:]
via a top-level Tauri command in src-tauri/src/macos/system_dictionary.rs.
Anchored at the selection's bottom-center (CSS pixels mapped into
NSView coords), so the inline Lookup HUD appears just below the
highlighted text without raising Dictionary.app to the foreground.
- iOS: UIReferenceLibraryViewController presented as a half-detent
pageSheet on iPhone (medium → large drag-to-expand) and as a
formSheet on iPad. Implemented in the native-bridge plugin.
- Android: ACTION_PROCESS_TEXT intent with EXTRA_PROCESS_TEXT_READONLY,
dispatched without createChooser so users get the standard system
disambiguation dialog with "Just once / Always" buttons. Reports
unavailable=true when no app handles the intent so the TS layer can
silently skip rather than open an empty chooser.
Web/Linux/Windows hide the row entirely. The provider is a sentinel —
the registry filters it out of the popup tab list (it has no in-popup
UI) and the annotator's handleDictionary checks isSystemDictionaryEnabled
to dispatch directly to the native bridge before opening the in-app
DictionaryPopup.
Add tauri-plugin-webview-upgrade as a git submodule under
apps/readest-app/src-tauri/plugins/. On Android devices whose system
WebView is locked to an old Chromium build (Huawei phones, Moaan / Onyx
/ Kobo e-ink readers, AOSP forks without Play Store, etc.), the reader
bundle renders as a blank screen. The plugin bootstraps before
Application.onCreate via androidx.startup and redirects the in-process
WebView loader to a recent com.google.android.webview when the user has
one sideloaded — opening the only window in which WebViewUpgrade can
swap the provider, before Tauri/Wry creates any WebView.
Thresholds (minUpgradeMajor / minSupportedMajor) come from
plugins.webview-upgrade in tauri.conf.json and are baked into Kotlin
constants at Gradle build time. Below the supported threshold with no
upgrade option, the plugin shows a localized AlertDialog (15 languages,
English fallback) prompting the user to install Android System WebView.
Plugin source: https://github.com/readest/tauri-plugin-webview-upgrade
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On macOS, closing the last window (Cmd+W or the red traffic light) was
quitting the app, which is unexpected — the native convention is that
Cmd+W closes the window while the app keeps running in the dock, and
only Cmd+Q quits.
Intercept the window CloseRequested event on macOS, prevent the close,
and hide the window instead so the app remains active. Handle the
Reopen event (fired when the user clicks the dock icon) to restore the
hidden window. Cmd+Q continues to go through applicationWillTerminate:
and exits the app normally.
Set up WebDriver-based testing for the Tauri app with two tiers:
- Vitest browser-mode tests (*.tauri.test.ts) running inside the Tauri WebView
for plugin IPC testing (libsql, smoke tests)
- WDIO E2E tests (*.e2e.ts) for UI-level interaction testing
Key changes:
- Add webdriver Cargo feature gating tauri-plugin-webdriver
- Add runtime capability for remote URLs (webdriver builds only)
- Add vitest.tauri.config.mts and wdio.conf.ts connecting to embedded
WebDriver server on port 4445
- Add shared tauri-invoke helper for IPC from Vitest iframe context
- Add testing documentation in docs/testing.md
* added current time to desktop bar
* added time prototype to footer, needs code cleanup and settings toggle
* fixed settings toggle, added translations and code cleanup
* added battery support and moved Statusbar to own Component
* #3306 added 24 hour clock support
* refactored code styling and getting rid of any type in battery hook
* Add battery info for Tauri Apps
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* feat(windows): Add Windows Explorer thumbnail support for ebooks (closes#2534)
- Implement IThumbnailProvider COM handler for Windows Shell integration
- Support EPUB, MOBI, AZW, AZW3, KF8, FB2, CBZ, CBR formats
- Add cover extraction with Readest icon overlay
- Register thumbnail handler via NSIS installer hooks
- Only show thumbnails when Readest is the default app for the file type
* chore: clean up build script for thumbnail extension
---------
Co-authored-by: chrox <chrox.huang@gmail.com>