diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 61b985d..185bd83 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,16 +37,13 @@ kon-cloud-providers = { path = "../crates/cloud-providers" } kon-hotkey = { path = "../crates/hotkey" } kon-llm = { path = "../crates/llm" } -# Tauri -tauri = { version = "2", features = ["tray-icon"] } +# Tauri. The `tray-icon` feature, the global-shortcut/window-state/ +# autostart plugins are desktop-only — gated below under +# cfg(not(target_os = "android")). The dialog, opener, and notification +# plugins all support Android natively so live in the unconditional list. +tauri = { version = "2" } tauri-plugin-opener = "2" tauri-plugin-dialog = "2" -tauri-plugin-global-shortcut = "2" -tauri-plugin-window-state = "2" -# Phase 5 rituals: register Corbie as a login-time autostart entry. -# Handles platform differences (.desktop file on Linux, LaunchAgents plist -# on macOS, registry Run key on Windows) behind a single API. -tauri-plugin-autostart = "2" # Phase 6 nudges: OS-native notifications via the frontend-owned nudge # bus. Permission-request flow happens on the JS side before the first # deliver_nudge call; Windows only delivers for installed apps. @@ -79,6 +76,25 @@ uuid = { version = "1", features = ["v4"] } # pollute the workspace. tempfile = "3" +# Desktop-only Tauri features and plugins. Each item below either fails +# to compile against the Android NDK or is a structural no-op on a +# single-window mobile app: +# - tray-icon: no system tray on mobile. +# - global-shortcut: mobile has no global hotkey API; the foreground +# notification with a record button is the Android replacement. +# - window-state: single-window, fullscreen, no per-window persistence. +# - autostart: mobile apps don't auto-start in the desktop sense; if +# we ever want a "boot completed" handler that's a separate Android +# intent receiver, not this plugin. +[target.'cfg(not(target_os = "android"))'.dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-global-shortcut = "2" +tauri-plugin-window-state = "2" +# Phase 5 rituals: register Corbie as a login-time autostart entry. +# Handles platform differences (.desktop file on Linux, LaunchAgents plist +# on macOS, registry Run key on Windows) behind a single API. +tauri-plugin-autostart = "2" + [target.'cfg(target_os = "linux")'.dependencies] webkit2gtk = "2.0" # Needed for setting the preview overlay's WindowTypeHint to Utility via diff --git a/src-tauri/src/commands/tts.rs b/src-tauri/src/commands/tts.rs index 2366a64..183acc1 100644 --- a/src-tauri/src/commands/tts.rs +++ b/src-tauri/src/commands/tts.rs @@ -287,6 +287,16 @@ fn list_voices_impl() -> Result, String> { .collect()) } +// Non-desktop fallback. Android's native TextToSpeech API would need a +// JNI bridge — out of scope for v0.1-android. The frontend already hides +// the Read Page Aloud button behind a runtime detection; this stub keeps +// the command surface consistent so a stray invoke surfaces a clear +// error rather than panicking on an unbound `list_voices_impl`. +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +fn list_voices_impl() -> Result, String> { + Ok(Vec::new()) +} + // ---------- Tauri commands ---------- #[tauri::command] @@ -310,13 +320,26 @@ pub fn tts_speak( let spawned = spawn_macos(trimmed, rate, voice.as_deref())?; #[cfg(target_os = "windows")] let spawned = spawn_windows(trimmed, rate, voice.as_deref())?; - - if let Some(c) = spawned { - if let Ok(mut guard) = state.child.lock() { - *guard = Some(c); - } + // No bundled TTS shim for non-desktop targets. Android has its own + // TextToSpeech APIs that would have to be reached over JNI; out of + // scope for v0.1-android. Mirrors the paste.rs not-implemented + // fallback so the command compiles cleanly across all targets and + // surfaces a clear error if the frontend tries to invoke it. + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + let _ = (state, rate, voice); + return Err("TTS not implemented on this platform".into()); + } + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + { + if let Some(c) = spawned { + if let Ok(mut guard) = state.child.lock() { + *guard = Some(c); + } + } + Ok(()) } - Ok(()) } #[tauri::command] diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 4f4abca..b7d141f 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -1,8 +1,46 @@ +// Multi-window support is desktop-only. Android Tauri apps run as a +// single Activity, so `WebviewWindowBuilder` is not available there. +// All four commands below have an Android stub that returns a clear +// error so frontend invokes don't panic — the frontend itself is +// expected to detect Android via `isAndroid()` and route the previously- +// secondary content (preview overlay, transcript viewer, task float) +// into routes inside the main window instead. +#[cfg(not(target_os = "android"))] use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +#[cfg(not(target_os = "android"))] use crate::PreferencesScript; +#[cfg(target_os = "android")] +const ANDROID_MULTIWINDOW_ERR: &str = + "Multi-window is not supported on Android; this command is desktop-only"; + +#[cfg(target_os = "android")] +#[tauri::command] +pub async fn open_task_window(_app: tauri::AppHandle) -> Result<(), String> { + Err(ANDROID_MULTIWINDOW_ERR.into()) +} + +#[cfg(target_os = "android")] +#[tauri::command] +pub async fn open_preview_window(_app: tauri::AppHandle) -> Result<(), String> { + Err(ANDROID_MULTIWINDOW_ERR.into()) +} + +#[cfg(target_os = "android")] +#[tauri::command] +pub async fn close_preview_window(_app: tauri::AppHandle) -> Result<(), String> { + Err(ANDROID_MULTIWINDOW_ERR.into()) +} + +#[cfg(target_os = "android")] +#[tauri::command] +pub async fn open_viewer_window(_app: tauri::AppHandle) -> Result<(), String> { + Err(ANDROID_MULTIWINDOW_ERR.into()) +} + /// Open a floating always-on-top task window. +#[cfg(not(target_os = "android"))] #[tauri::command] pub async fn open_task_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("tasks-float") { @@ -46,6 +84,7 @@ pub async fn open_task_window(app: tauri::AppHandle) -> Result<(), String> { /// The preview is passive: it subscribes to the `transcription-result` /// event and the cross-window `preview-*` events that DictationPage fires /// as it moves through the phases of a dictation run. +#[cfg(not(target_os = "android"))] #[tauri::command] pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("transcription-preview") { @@ -115,6 +154,7 @@ pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> { /// Hide the transcription preview window without destroying it so the next /// open is instant. Returns Ok even when no preview window exists. +#[cfg(not(target_os = "android"))] #[tauri::command] pub async fn close_preview_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("transcription-preview") { @@ -124,6 +164,7 @@ pub async fn close_preview_window(app: tauri::AppHandle) -> Result<(), String> { } /// Open the transcript viewer window. +#[cfg(not(target_os = "android"))] #[tauri::command] pub async fn open_viewer_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("transcript-viewer") { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 969ab9f..0bf9b65 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,7 @@ mod commands; +// System tray uses Tauri's `tray-icon` feature which is desktop-only. +// Android has no tray surface — drop the module entirely on that target. +#[cfg(not(target_os = "android"))] mod tray; use std::sync::Arc; @@ -132,9 +135,23 @@ pub fn run() { // Settings → About can attach them. Local only; nothing transmitted. commands::diagnostics::install_panic_hook(); - tauri::Builder::default() + let builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) + // Phase 6 nudges: OS-native notifications. The plugin exposes + // isPermissionGranted / requestPermission / sendNotification on + // the JS side; our deliver_nudge wrapper guards those calls + // against the nudge-bus suppression rules and restricts + // invocation to the main window via ensure_main_window. + .plugin(tauri_plugin_notification::init()); + + // Desktop-only plugins. Each is either unsupported on Android + // (global-shortcut, autostart) or structurally meaningless on a + // single-window mobile app (window-state). Gating here matches the + // Cargo.toml `cfg(not(target_os = "android"))` block that drops the + // crates entirely on Android targets. + #[cfg(not(target_os = "android"))] + let builder = builder .plugin(tauri_plugin_global_shortcut::Builder::new().build()) // Phase 5 rituals: autostart. The plugin registers JS-facing // commands (isEnabled / enable / disable) that the Settings @@ -145,18 +162,14 @@ pub fn run() { tauri_plugin_autostart::MacosLauncher::LaunchAgent, None, )) - // Phase 6 nudges: OS-native notifications. The plugin exposes - // isPermissionGranted / requestPermission / sendNotification on - // the JS side; our deliver_nudge wrapper guards those calls - // against the nudge-bus suppression rules and restricts - // invocation to the main window via ensure_main_window. - .plugin(tauri_plugin_notification::init()) // Remember size + position of every window across app restarts. // Without this, secondary windows (preview overlay, task float, // transcript viewer) open at whatever spot the compositor picks, // which feels random. State is persisted per-window-label to // app-data/window-state.json. - .plugin(tauri_plugin_window_state::Builder::default().build()) + .plugin(tauri_plugin_window_state::Builder::default().build()); + + builder .setup(|app| { // Initialise database (blocking in setup — runs once at startup) let db_path = database_path(); @@ -256,14 +269,20 @@ pub fn run() { }); } - // Close-to-tray: hide window instead of exiting - let win = main_window.clone(); - main_window.on_window_event(move |event| { - if let tauri::WindowEvent::CloseRequested { api, .. } = event { - api.prevent_close(); - let _ = win.hide(); - } - }); + // Close-to-tray: hide the window instead of exiting so the + // tray icon stays as the canonical entry point. Desktop-only; + // mobile has no tray and "hide on close" maps to the + // platform's own background-app behaviour. + #[cfg(not(target_os = "android"))] + { + let win = main_window.clone(); + main_window.on_window_event(move |event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = win.hide(); + } + }); + } } // Store init script for secondary windows (float, viewer) @@ -287,6 +306,7 @@ pub fn run() { // hint. No-ops on a fully-supported box. crate::commands::models::emit_runtime_warnings(app.handle()); + #[cfg(not(target_os = "android"))] if let Err(e) = tray::setup(app) { eprintln!("Failed to setup tray: {e}"); }