//! Paarrot Desktop library for cross-platform builds including Android #[cfg(mobile)] mod mobile; #[cfg(not(any(target_os = "android", target_os = "ios")))] use tauri::{ Manager, WebviewUrl, menu::{Menu, MenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, webview::WebviewWindowBuilder, WindowEvent, }; // Linux: Import for permission handling #[cfg(target_os = "linux")] use webkit2gtk::{PermissionRequestExt, WebViewExt}; #[cfg(target_os = "linux")] use gtk::prelude::*; /// Read image from clipboard on Linux using arboard with Wayland support #[cfg(target_os = "linux")] #[tauri::command] fn read_clipboard_image() -> Result, String> { use arboard::Clipboard; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?; match clipboard.get_image() { Ok(img) => { // Convert RGBA image data to PNG let width = img.width as u32; let height = img.height as u32; let mut png_data = Vec::new(); { let mut encoder = png::Encoder::new(&mut png_data, width, height); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder.write_header().map_err(|e| e.to_string())?; writer.write_image_data(&img.bytes).map_err(|e| e.to_string())?; } let base64_data = BASE64.encode(&png_data); Ok(Some(format!("data:image/png;base64,{}", base64_data))) } Err(_) => Ok(None), // No image in clipboard } } /// Stub for non-Linux platforms - returns None #[cfg(not(target_os = "linux"))] #[tauri::command] fn read_clipboard_image() -> Result, String> { Ok(None) } /// Open a URL in the default browser (bypasses ACL issues with localhost plugin) #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tauri::command] fn open_external_url(url: String) -> Result<(), String> { open::that(&url).map_err(|e| e.to_string()) } /// Runs the Tauri application pub fn run() { #[cfg(not(any(target_os = "android", target_os = "ios")))] { let port = 44548; tauri::Builder::default() .plugin(tauri_plugin_localhost::Builder::new(port).build()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { // When a second instance tries to launch, focus the existing window if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } })) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--minimized"]), )) .setup(move |app| { // In dev mode, use Vite dev server; in production, use localhost plugin let url: tauri::Url = if cfg!(dev) { "http://localhost:8080".parse().unwrap() } else { format!("http://localhost:{}", port).parse().unwrap() }; // Create the main window manually with navigation handler for external links WebviewWindowBuilder::new(app, "main", WebviewUrl::External(url)) .title("Paarrot") .inner_size(1280.0, 905.0) .center() .resizable(true) .disable_drag_drop_handler() .on_navigation(|url| { let url_str = url.as_str(); // Allow navigation to localhost (our app) and special protocols if url_str.starts_with("http://localhost") || url_str.starts_with("https://localhost") || url_str.starts_with("tauri://") || url_str.starts_with("blob:") || url_str.starts_with("data:") { return true; } // Block external URLs - open them in default browser if url_str.starts_with("http://") || url_str.starts_with("https://") { let _ = tauri_plugin_opener::open_url(url_str, None::<&str>); return false; } true }) .build()?; // Linux: Set up permission handler to auto-allow microphone/camera access #[cfg(target_os = "linux")] { if let Some(webview_window) = app.get_webview_window("main") { let _ = webview_window.with_webview(|webview| { use webkit2gtk::UserMediaPermissionRequestExt; let wv = webview.inner(); wv.connect_permission_request(|_webview, permission_request| { // Check if this is a user media (microphone/camera) permission request if let Some(user_media_request) = permission_request.downcast_ref::() { // Log what's being requested let is_audio = user_media_request.is_for_audio_device(); let is_video = user_media_request.is_for_video_device(); eprintln!("Paarrot: Media permission request - audio: {}, video: {}", is_audio, is_video); // Allow the request permission_request.allow(); return true; } // For other permission types, allow by default permission_request.allow(); true }); }); } } // Create system tray let show_item = MenuItem::with_id(app, "show", "Show Paarrot", true, None::<&str>)?; let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show_item, &quit_item])?; let _tray = TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .show_menu_on_left_click(false) .on_menu_event(|app, event| match event.id.as_ref() { "show" => { if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } } "quit" => { app.exit(0); } _ => {} }) .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { let app = tray.app_handle(); if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } } }) .build(app)?; Ok(()) }) .on_window_event(|window, event| { // Minimize to tray on close instead of quitting if let WindowEvent::CloseRequested { api, .. } = event { window.hide().unwrap(); api.prevent_close(); } }) .invoke_handler(tauri::generate_handler![read_clipboard_image, open_external_url]) .run(tauri::generate_context!()) .expect("error while building tauri application"); } #[cfg(any(target_os = "android", target_os = "ios"))] { use tauri::{WebviewUrl, webview::WebviewWindowBuilder}; tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_deep_link::init()) .setup(|app| { // Create the main window for mobile // Mobile uses the bundled assets directly, not localhost WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) .build()?; Ok(()) }) .invoke_handler(tauri::generate_handler![read_clipboard_image]) .run(tauri::generate_context!()) .expect("error while building tauri application"); } }