diff --git a/Cargo.lock b/Cargo.lock index 687d119c..72472b22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,7 @@ dependencies = [ "tauri", "tauri-build 2.6.2", "tauri-plugin-cli", + "tauri-plugin-clipboard-manager", "tauri-plugin-deep-link", "tauri-plugin-device-info", "tauri-plugin-dialog", @@ -273,6 +274,27 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "arc-swap" version = "1.9.1" @@ -958,7 +980,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1072,6 +1094,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "cocoa" version = "0.25.0" @@ -1823,6 +1854,12 @@ dependencies = [ "tendril 0.5.0", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "downcast-rs" version = "2.0.2" @@ -2013,6 +2050,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -2063,6 +2106,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -2116,6 +2165,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -2820,6 +2875,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3230,6 +3296,7 @@ dependencies = [ "moxcms", "num-traits", "png 0.18.1", + "tiff", ] [[package]] @@ -4197,6 +4264,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "notify" version = "8.2.0" @@ -4974,6 +5050,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + [[package]] name = "phf" version = "0.8.0" @@ -5469,6 +5556,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.39.4" @@ -6196,7 +6289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", - "quick-error", + "quick-error 1.2.3", "tempfile", "wait-timeout", ] @@ -7112,7 +7205,7 @@ dependencies = [ "crc32fast", "crossbeam-channel", "datasketches", - "downcast-rs", + "downcast-rs 2.0.2", "fastdivide", "fnv", "fs4", @@ -7164,7 +7257,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc" dependencies = [ - "downcast-rs", + "downcast-rs 2.0.2", "fastdivide", "itertools 0.14.0", "serde", @@ -7205,7 +7298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82" dependencies = [ "fnv", - "nom", + "nom 7.1.3", "ordered-float", "serde", "serde_json", @@ -7493,6 +7586,21 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-deep-link" version = "2.4.9" @@ -8160,6 +8268,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -8567,6 +8689,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom 8.0.0", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -9245,6 +9378,76 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs 1.2.1", + "rustix 1.1.4", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.99" @@ -9384,6 +9587,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "which" version = "4.4.2" @@ -10185,6 +10394,24 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "wmi" version = "0.13.4" @@ -10281,6 +10508,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "xattr" version = "1.6.1" @@ -10526,6 +10770,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.12.0" diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index b9855a2d..aca22ac5 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -124,6 +124,7 @@ "@supabase/supabase-js": "^2.76.1", "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-cli": "^2.4.1", + "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-deep-link": "^2.4.7", "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml index 58fe5a41..2051afad 100644 --- a/apps/readest-app/src-tauri/Cargo.toml +++ b/apps/readest-app/src-tauri/Cargo.toml @@ -59,6 +59,7 @@ tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" } tauri-plugin-webview-upgrade = { path = "./plugins/tauri-plugin-webview-upgrade" } tauri-plugin-websocket = "2" tauri-plugin-sharekit = "0.3" +tauri-plugin-clipboard-manager = "2" tauri-plugin-device-info = "1.0.1" tauri-plugin-turso = { path = "./plugins/tauri-plugin-turso" } tauri-plugin-webdriver = { version = "0.2", optional = true } diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 97ade28d..2adb844f 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -196,6 +196,8 @@ "haptics:allow-selection-feedback", "deep-link:default", "sharekit:default", + "clipboard-manager:allow-write-text", + "clipboard-manager:allow-read-text", "device-info:default", "turso:default", "native-tts:default", diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index 409c4f2d..1e0c798b 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -290,6 +290,7 @@ pub fn run() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_sharekit::init()) + .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_device_info::init()) .plugin(tauri_plugin_turso::init()) .plugin(tauri_plugin_native_bridge::init()) diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index 25184bc2..f081a1ce 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -45,6 +45,7 @@ import { import { runSimpleCC } from '@/utils/simplecc'; import { getWordCount } from '@/utils/word'; import { getIndexFromCfi, isCfiInLocation } from '@/utils/cfi'; +import { writeTextToClipboard } from '@/utils/clipboard'; import { TransformContext } from '@/services/transformers/types'; import { transformContent } from '@/services/transformService'; import { @@ -729,9 +730,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const handleCopy = (dismissPopup = true) => { if (!selection || !selection.text) return; + const textToCopy = selection.text; setTimeout(() => { // Delay to ensure it won't be overridden by system clipboard actions - navigator.clipboard?.writeText(selection.text); + void writeTextToClipboard(textToCopy); }, 100); if (dismissPopup) { handleDismissPopupAndSelection(); @@ -1208,7 +1210,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { setTimeout(() => { // Delay to ensure it won't be overridden by system clipboard actions - navigator.clipboard?.writeText(content); + void writeTextToClipboard(content); }, 100); const ext = isPlainText ? 'txt' : 'md'; diff --git a/apps/readest-app/src/utils/clipboard.ts b/apps/readest-app/src/utils/clipboard.ts new file mode 100644 index 00000000..8f4de1c1 --- /dev/null +++ b/apps/readest-app/src/utils/clipboard.ts @@ -0,0 +1,62 @@ +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 => { + 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; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28fdb340..ae5c8448 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: '@tauri-apps/plugin-cli': specifier: ^2.4.1 version: 2.4.1 + '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.3.0 + version: 2.3.2 '@tauri-apps/plugin-deep-link': specifier: ^2.4.7 version: 2.4.9 @@ -3588,6 +3591,9 @@ packages: '@tauri-apps/plugin-cli@2.4.1': resolution: {integrity: sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==} + '@tauri-apps/plugin-clipboard-manager@2.3.2': + resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} + '@tauri-apps/plugin-deep-link@2.4.9': resolution: {integrity: sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA==} @@ -12070,6 +12076,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-clipboard-manager@2.3.2': + dependencies: + '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-deep-link@2.4.9': dependencies: '@tauri-apps/api': 2.11.0