forked from akai/readest
feat(dictionary): add system dictionary provider for macOS, iOS, and Android (#4219)
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.
This commit is contained in:
@@ -166,6 +166,19 @@ pub(crate) async fn open_external_url<R: Runtime>(
|
||||
app.native_bridge().open_external_url(payload)
|
||||
}
|
||||
|
||||
/// See [`ShowLookupPopoverRequest`] in `models.rs` for platform-by-
|
||||
/// platform behavior. The mobile bridge dispatches into the iOS /
|
||||
/// Android plugin; desktop returns `UnsupportedPlatformError` and the
|
||||
/// TS layer keeps the macOS-specific path going through the
|
||||
/// top-level `show_lookup_popover` Tauri command (AppKit HUD).
|
||||
#[command]
|
||||
pub(crate) async fn show_lookup_popover<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: ShowLookupPopoverRequest,
|
||||
) -> Result<ShowLookupPopoverResponse> {
|
||||
app.native_bridge().show_lookup_popover(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn select_directory<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
|
||||
@@ -166,6 +166,19 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
/// Desktop has no mobile-style "system dictionary intent" surface;
|
||||
/// macOS's HUD is invoked through a separate top-level Tauri
|
||||
/// command (`show_lookup_popover` in `src/macos/system_dictionary.rs`),
|
||||
/// and Linux/Windows have no native target. Return
|
||||
/// UnsupportedPlatformError here so the TS layer doesn't
|
||||
/// accidentally dispatch through the mobile plugin on desktop.
|
||||
pub fn show_lookup_popover(
|
||||
&self,
|
||||
_payload: ShowLookupPopoverRequest,
|
||||
) -> crate::Result<ShowLookupPopoverResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn select_directory(&self) -> crate::Result<SelectDirectoryResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::set_screen_brightness,
|
||||
commands::get_external_sdcard_path,
|
||||
commands::open_external_url,
|
||||
commands::show_lookup_popover,
|
||||
commands::select_directory,
|
||||
commands::get_storefront_region_code,
|
||||
commands::request_manage_storage_permission,
|
||||
|
||||
@@ -216,6 +216,17 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn show_lookup_popover(
|
||||
&self,
|
||||
payload: ShowLookupPopoverRequest,
|
||||
) -> crate::Result<ShowLookupPopoverResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("show_lookup_popover", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn select_directory(&self) -> crate::Result<SelectDirectoryResponse> {
|
||||
self.0
|
||||
|
||||
@@ -222,6 +222,36 @@ pub struct OpenExternalUrlResponse {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Hand a word off to the platform's native dictionary surface.
|
||||
///
|
||||
/// On iOS this presents `UIReferenceLibraryViewController` modally
|
||||
/// (the same UI Apple uses for `Look Up` in UIKit text views). On
|
||||
/// Android it dispatches `ACTION_PROCESS_TEXT` so any installed
|
||||
/// dictionary app (ColorDict, GoldenDict, 欧路, etc.) can handle the
|
||||
/// word; we don't bind to a specific package so users can stick with
|
||||
/// their preferred dictionary. Desktop platforms return
|
||||
/// `UnsupportedPlatformError` — macOS goes through a separate native
|
||||
/// command in `src/macos/system_dictionary.rs` that uses the AppKit
|
||||
/// HUD surface, which doesn't exist on iOS/Android.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ShowLookupPopoverRequest {
|
||||
pub word: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ShowLookupPopoverResponse {
|
||||
pub success: bool,
|
||||
/// `unavailable` is set on Android when no app responded to the
|
||||
/// `ACTION_PROCESS_TEXT` intent (i.e. the user has no dictionary
|
||||
/// installed). The TS layer can surface a "no dictionary app"
|
||||
/// hint without us having to push a localized string from
|
||||
/// native code.
|
||||
pub unavailable: Option<bool>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SelectDirectoryResponse {
|
||||
|
||||
Reference in New Issue
Block a user