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:
@@ -15,6 +15,8 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useCustomDictionaryStore } from '@/store/customDictionaryStore';
|
||||
import { isSystemDictionaryEnabled } from '@/services/dictionaries/registry';
|
||||
import { invokeSystemDictionary } from '@/services/dictionaries/systemDictionary';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
@@ -24,7 +26,13 @@ import { useReadwiseSync } from '../../hooks/useReadwiseSync';
|
||||
import { useHardcoverSync } from '../../hooks/useHardcoverSync';
|
||||
import { useTextSelector } from '../../hooks/useTextSelector';
|
||||
import { Point, Position, TextSelection } from '@/utils/sel';
|
||||
import { getPopupPosition, getPosition, getTextFromRange } from '@/utils/sel';
|
||||
import {
|
||||
getPopupPosition,
|
||||
getPosition,
|
||||
getRangeRectInWebview,
|
||||
getRangeTextStyleInWebview,
|
||||
getTextFromRange,
|
||||
} from '@/utils/sel';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { findTocItemBS } from '@/services/nav';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
@@ -793,6 +801,27 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const handleDictionary = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
// System-dictionary path: when the user has opted in via Settings →
|
||||
// Languages → Dictionaries, hand the selection to the OS instead of
|
||||
// opening the in-app popup. Exclusivity is enforced at the store
|
||||
// level (enabling system disables everything else and vice versa),
|
||||
// so a single check on the system flag is sufficient.
|
||||
const dictSettings = useCustomDictionaryStore.getState().settings;
|
||||
if (isSystemDictionaryEnabled(dictSettings)) {
|
||||
// Build the macOS HUD anchor: the selection rect (so the HUD
|
||||
// appears at the original word) and the underlying paragraph's
|
||||
// text style (so AppKit re-draws the small label at the same
|
||||
// font size / colour as the original, matching the system
|
||||
// right-click → Look Up presentation).
|
||||
const rect = selection.range ? getRangeRectInWebview(selection.range) : null;
|
||||
const style = selection.range ? getRangeTextStyleInWebview(selection.range) : null;
|
||||
void invokeSystemDictionary(
|
||||
selection.text,
|
||||
rect ? { rect, style: style ?? undefined } : undefined,
|
||||
);
|
||||
handleDismissPopupAndSelection();
|
||||
return;
|
||||
}
|
||||
setShowAnnotPopup(false);
|
||||
setShowDictionaryPopup(true);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user