iOS and Android now run the same Web-URL clip flow as desktop. Paste
an article URL, the native side opens a full-screen WKWebView /
WebView with the same Chrome UA + fingerprint mask + "Saving to
Readest" overlay as the desktop hidden window, waits for load +
settle, captures `document.documentElement.outerHTML` via the
platform's `evaluateJavaScript`, and returns it through the existing
`convertToEpub` pipeline.
JS surface stays `invoke('clip_url', { url, options })` — no changes
in `library/page.tsx` or `send/page.tsx`. The platform branch lives
entirely in `clip_url.rs`.
Why not Tauri's `Window::add_child`
`add_child` is gated `#[cfg(any(test, all(desktop, feature =
"unstable")))]` in tauri 2.10. No public API for attaching a second
webview to the main window on mobile, so the clip flow can't be a
`#[cfg(mobile)]` branch of the existing `WebviewWindowBuilder` shape
— it needs native code. Extend `tauri-plugin-native-bridge` rather
than create a separate plugin: the Swift / Kotlin scaffolding +
Tauri IPC are already there.
Layout
- `src-tauri/src/clip_url.rs` — desktop branch unchanged; new
`#[cfg(mobile)]` `clip_url` command routes through
`app.native_bridge().clip_url(request)`. Shared `ClipOptions`
struct exposes its fields `pub` so the mobile branch can map into
the plugin's `ClipUrlRequest`.
- `plugins/tauri-plugin-native-bridge/src/models.rs` — `ClipUrlRequest`
+ `ClipUrlResponse` mirroring `ClipOptions` field-for-field so the
payload travels untouched from JS through to Swift/Kotlin.
- `plugins/tauri-plugin-native-bridge/src/{desktop,mobile}.rs` — desktop
returns an error (desktop has its own path); mobile dispatches via
`run_mobile_plugin("clip_url", payload)`.
- `ios/Sources/ClipUrlController.swift` — `UIViewController` hosting
`WKWebView` with the loading overlay drawn as native UIKit views
(not an injected user script, so the page's own hydration can't
wipe the spinner). 30 s hard timeout + 3 s settle window after
`didFinish`, same as desktop. Fingerprint mask injected as
`WKUserScript` at `.atDocumentStart`.
- `android/src/main/java/ClipUrlController.kt` — full-screen Dialog
hosting a `WebView`, mirrors the iOS controller's behaviour. JSON-
decodes the `evaluateJavascript` callback (raw return value is a
JSON-encoded string).
- `NativeBridgePlugin.{swift,kt}` — new `clip_url` method that parses
args via `invoke.parseArgs`, presents the controller, resolves the
invoke with `{ html }` on success or `invoke.reject` on failure.
Same rejection vocabulary as desktop (`"Invalid URL"`, `"Page took
too long to load"`, etc.) so the calling JS doesn't need a
platform branch.
- `build.rs` — adds `clip_url` to the plugin's `COMMANDS` array.
Notes
- The Swift overlay reserves the iOS safe-area-edge-to-edge so notch /
Dynamic Island devices don't see the underlying app peek through
during the brief capture window.
- The Android overlay's spinner tint follows the foreground theme
colour at 85 % alpha — same idea as the iOS controller.
- `WKWebView`'s JS keeps running while the controller is presented;
no off-screen / `isHidden` trick that would let iOS throttle the
page mid-capture.
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.
To enable background playback in Android, go to Settings > Apps & Notifications > Readest > Battery > Battery Optimization, and disable battery optimization for Readest.