Files
readest/apps/readest-app/src/utils/clipboard.ts
T
loveheaven 726f53a64b fix(reader): use Tauri clipboard plugin for copy on Android (#4409)
navigator.clipboard.writeText is unreliable inside the Tauri Android
WebView, so tapping Copy in the Reader's selection popup silently
no-ops. Route the write through @tauri-apps/plugin-clipboard-manager
on Tauri targets (Android, iOS, macOS, Windows, Linux), with a
graceful navigator.clipboard / execCommand fallback for the web
build and older WebViews.

- Add tauri-plugin-clipboard-manager (Rust + JS)
- Register the plugin in lib.rs
- Grant clipboard-manager:allow-write-text / allow-read-text
- New utils/clipboard.ts wrapper with platform-aware fallback chain
- Annotator handleCopy and handleConfirmExport use the wrapper
2026-06-02 16:02:52 +02:00

63 lines
2.2 KiB
TypeScript

import { isTauriAppPlatform } from '@/services/environment';
/**
* Write `text` to the system clipboard.
*
* Why this wrapper exists:
* On Android (Tauri WebView) `navigator.clipboard.writeText` does not
* reliably write to the OS clipboard — selecting text inside a Reader
* iframe and tapping the in-app Copy button silently no-ops. Routing
* through the Tauri `clipboard-manager` plugin uses the Android
* `ClipboardManager` API directly and works in every Tauri target
* (Android, iOS, macOS, Windows, Linux). On the web build we keep the
* standard `navigator.clipboard` path with an `execCommand('copy')`
* fallback for older browsers / non-secure contexts.
*
* Resolves to `true` when the write succeeded, `false` otherwise.
*/
export const writeTextToClipboard = async (text: string): Promise<boolean> => {
if (typeof text !== 'string') return false;
if (isTauriAppPlatform()) {
try {
const { writeText } = await import('@tauri-apps/plugin-clipboard-manager');
await writeText(text);
return true;
} catch (err) {
// Fall through to web APIs below — better to silently degrade than
// to lose the user's copy entirely if the plugin is unavailable.
console.warn('[clipboard] tauri writeText failed, falling back', err);
}
}
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn('[clipboard] navigator.clipboard.writeText failed, falling back', err);
}
}
// Last-resort fallback for non-secure contexts / older WebViews.
if (typeof document !== 'undefined') {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
ta.style.pointerEvents = 'none';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
} catch (err) {
console.warn('[clipboard] execCommand fallback failed', err);
}
}
return false;
};