mod crypto; mod screen_capture; mod screen_sources; #[cfg(feature = "rust-livekit")] mod livekit_bridge; #[cfg(not(any(target_os = "android", target_os = "ios")))] use tauri::{ menu::{Menu, MenuEvent, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Listener, Manager, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use serde::Deserialize; // Payload for the `tray-unread-update` event the JS layer emits whenever the // aggregate unread-count changes. 0 hides the badge / resets the tooltip; // non-zero sets a count indicator. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[derive(Deserialize)] struct TrayUnreadPayload { count: u32, } // Red-dot overlay icon for the Windows taskbar. Drawn as raw RGBA instead of // shipping a PNG so we don't add another resource to the bundle. Kept small // (32x32) since Windows scales the overlay down anyway. #[cfg(target_os = "windows")] fn unread_overlay_rgba() -> Vec { const SIZE: u32 = 32; let r = SIZE as f32 / 2.0; let mut buf = Vec::with_capacity((SIZE * SIZE * 4) as usize); for y in 0..SIZE { for x in 0..SIZE { let dx = x as f32 - r + 0.5; let dy = y as f32 - r + 0.5; let d = (dx * dx + dy * dy).sqrt(); let edge = r - 1.0; if d <= edge { buf.extend_from_slice(&[0xDC, 0x26, 0x26, 0xFF]); } else if d <= r { let alpha = (255.0 * (r - d)).clamp(0.0, 255.0) as u8; buf.extend_from_slice(&[0xDC, 0x26, 0x26, alpha]); } else { buf.extend_from_slice(&[0, 0, 0, 0]); } } } buf } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn show_main_window(app: &AppHandle) { if let Some(win) = app.get_webview_window("main") { let _ = win.show(); let _ = win.unminimize(); let _ = win.set_focus(); } } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn hide_main_window(app: &AppHandle) { if let Some(win) = app.get_webview_window("main") { let _ = win.hide(); } } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_menu_event(app: &AppHandle, event: MenuEvent) { match event.id.as_ref() { "tray-show" => show_main_window(app), "tray-hide" => hide_main_window(app), "tray-quit" => { app.exit(0); } _ => {} } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { #[cfg(not(feature = "rust-livekit"))] let mut builder = tauri::Builder::default() .invoke_handler(tauri::generate_handler![ crypto::crypto_random_bytes, crypto::crypto_secretbox_encrypt, crypto::crypto_secretbox_decrypt, crypto::crypto_box_keypair, crypto::crypto_box_encrypt, crypto::crypto_box_decrypt, crypto::crypto_box_seal, crypto::crypto_box_seal_open, crypto::crypto_pwhash, screen_sources::enumerate_screen_sources, screen_sources::list_screen_sources, screen_sources::capture_screen_source_thumbnail, screen_capture::start_screen_capture, screen_capture::stop_screen_capture, ]) .plugin(tauri_plugin_notification::init()); #[cfg(feature = "rust-livekit")] let mut builder = tauri::Builder::default() .manage(livekit_bridge::LivekitState::new()) .invoke_handler(tauri::generate_handler![ crypto::crypto_random_bytes, crypto::crypto_secretbox_encrypt, crypto::crypto_secretbox_decrypt, crypto::crypto_box_keypair, crypto::crypto_box_encrypt, crypto::crypto_box_decrypt, crypto::crypto_box_seal, crypto::crypto_box_seal_open, crypto::crypto_pwhash, screen_sources::enumerate_screen_sources, screen_sources::list_screen_sources, screen_sources::capture_screen_source_thumbnail, screen_capture::start_screen_capture, screen_capture::stop_screen_capture, livekit_bridge::livekit_connect, livekit_bridge::livekit_disconnect, livekit_bridge::livekit_send_data, livekit_bridge::livekit_set_mic, livekit_bridge::livekit_set_camera, ]) .plugin(tauri_plugin_notification::init()); builder = builder .plugin(tauri_plugin_sql::Builder::default().build()) .plugin(tauri_plugin_fs::init()) .plugin( tauri_plugin_stronghold::Builder::new(|password| { // TODO: derive stronghold key from password using argon2 / blake2b. // Placeholder so project compiles. password.as_bytes().to_vec() }) .build(), ); // Global shortcut + updater + window-state plugins are desktop-only // (no mobile support — mobile windows are OS-managed). #[cfg(not(any(target_os = "android", target_os = "ios")))] { builder = builder .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_window_state::Builder::new().build()); } #[cfg(not(any(target_os = "android", target_os = "ios")))] { builder = builder.setup(|app| { // Tray icon with a minimal menu. Left-click toggles window // visibility; right-click shows the menu. Badge / tooltip updates // come from the JS side via `tray-unread-update` events. let show = MenuItem::with_id(app, "tray-show", "Öffnen", true, None::<&str>)?; let hide = MenuItem::with_id(app, "tray-hide", "Ausblenden", true, None::<&str>)?; let sep = PredefinedMenuItem::separator(app)?; let quit = MenuItem::with_id(app, "tray-quit", "Beenden", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show, &hide, &sep, &quit])?; let mut tray_builder = TrayIconBuilder::with_id("chatapp-tray") .menu(&menu) .show_menu_on_left_click(false) .tooltip("ChatApp") .on_menu_event(|app, event| handle_menu_event(app, event)) .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(win) = app.get_webview_window("main") { if win.is_visible().unwrap_or(false) { let _ = win.hide(); } else { let _ = win.show(); let _ = win.set_focus(); } } } }); // `default_window_icon` returns Option<&Image>; only attach if // we actually have one bundled (should always be true via the // tauri.conf.json icon list, but guard to stay typesafe). if let Some(icon) = app.default_window_icon() { tray_builder = tray_builder.icon(icon.clone()); } let tray = tray_builder.build(app)?; // Listen for JS-side unread updates and mirror them into the tray // tooltip + macOS dock badge. `tray` is cheap to clone (internal // Arc) so we can move it into the listener closure directly. let tray_handle = tray.clone(); let badge_window = app.get_webview_window("main"); app.listen("tray-unread-update", move |event| { let Ok(payload) = serde_json::from_str::(event.payload()) else { return; }; let tooltip = if payload.count == 0 { "ChatApp".to_string() } else { format!("ChatApp · {} neu", payload.count) }; let _ = tray_handle.set_tooltip(Some(tooltip)); // Dock/taskbar badge. macOS uses a numeric label; Windows uses // an overlay icon (red dot = unread). Linux has no cross-DE // badge API — skip. #[cfg(target_os = "macos")] if let Some(win) = badge_window.as_ref() { let badge = if payload.count == 0 { None } else { Some(payload.count.to_string()) }; let _ = win.set_badge_label(badge); } #[cfg(target_os = "windows")] if let Some(win) = badge_window.as_ref() { if payload.count == 0 { let _ = win.set_overlay_icon(None); } else { let rgba = unread_overlay_rgba(); let img = tauri::image::Image::new_owned(rgba, 32, 32); let _ = win.set_overlay_icon(Some(img)); } } #[cfg(not(any(target_os = "macos", target_os = "windows")))] let _ = &badge_window; }); Ok(()) }); } builder .run(tauri::generate_context!()) .expect("error while running tauri application"); }