| use serde_json; |
| use std::env; |
| use std::sync::{Arc, Mutex}; |
| use tauri::{ |
| Emitter, Listener, Manager, PhysicalPosition, Position, TitleBarStyle, WebviewUrl, |
| WebviewWindowBuilder, |
| }; |
| use tauri_plugin_deep_link::DeepLinkExt; |
| use tauri_plugin_updater; |
| use tauri_plugin_dialog; |
| use tauri_plugin_process; |
| use tauri::menu::{Menu, MenuItem}; |
| use tauri::image::Image; |
| use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; |
| use image; |
|
|
| |
| type SearchWindowState = Arc<Mutex<bool>>; |
|
|
| #[tauri::command] |
| fn show_window(window: tauri::Window) -> Result<(), String> { |
| |
| let app_handle = window.app_handle(); |
| let main_window = match app_handle.get_webview_window("main") { |
| Some(window) => window, |
| None => { |
| return Err("Main window not found".to_string()); |
| } |
| }; |
|
|
| main_window |
| .show() |
| .map_err(|e| format!("Failed to show window: {}", e))?; |
| main_window |
| .set_focus() |
| .map_err(|e| format!("Failed to set focus: {}", e))?; |
|
|
| Ok(()) |
| } |
|
|
| |
| |
| #[cfg(desktop)] |
| async fn prompt_and_install_update(app: &tauri::AppHandle, update: tauri_plugin_updater::Update) { |
| use tauri_plugin_dialog::{DialogExt, MessageDialogKind, MessageDialogButtons}; |
|
|
| let answer = app.dialog() |
| .message(format!("A new version {} is available. Would you like to update now?", update.version)) |
| .title("Update Available") |
| .kind(MessageDialogKind::Info) |
| .buttons(MessageDialogButtons::OkCancel) |
| .blocking_show(); |
|
|
| if answer { |
| let _ = update.download_and_install( |
| |_chunk_length, _content_length| {}, |
| || { |
| println!("Update download finished"); |
| } |
| ).await; |
| } |
| } |
|
|
| |
| |
| #[cfg(desktop)] |
| async fn silent_update_check(app: tauri::AppHandle) { |
| use tauri_plugin_updater::UpdaterExt; |
|
|
| if let Ok(updater) = app.updater() { |
| match updater.check().await { |
| Ok(Some(update)) => { |
| println!("Update available: {}", update.version); |
| prompt_and_install_update(&app, update).await; |
| } |
| Ok(None) => { |
| println!("No updates available"); |
| } |
| Err(e) => { |
| println!("Silent update check failed: {}", e); |
| } |
| } |
| } |
| } |
|
|
| |
| |
| #[tauri::command] |
| async fn check_for_updates(app: tauri::AppHandle) -> Result<(), String> { |
| use tauri_plugin_dialog::{DialogExt, MessageDialogKind, MessageDialogButtons}; |
| use tauri_plugin_updater::UpdaterExt; |
| |
| #[cfg(desktop)] |
| { |
| if let Ok(updater) = app.updater() { |
| match updater.check().await { |
| Ok(Some(update)) => { |
| prompt_and_install_update(&app, update).await; |
| } |
| Ok(None) => { |
| let version = app.package_info().version.to_string(); |
| app.dialog() |
| .message(format!("Midday\nversion {}\n\nYou're up to date!", version)) |
| .title("No Updates Available") |
| .kind(MessageDialogKind::Info) |
| .buttons(MessageDialogButtons::Ok) |
| .blocking_show(); |
| } |
| Err(e) => { |
| app.dialog() |
| .message(format!("Failed to check for updates: {}", e)) |
| .title("Update Check Failed") |
| .kind(MessageDialogKind::Error) |
| .buttons(MessageDialogButtons::Ok) |
| .blocking_show(); |
| } |
| } |
| } else { |
| app.dialog() |
| .message("Update checking is not available in this build.") |
| .title("Updates Not Available") |
| .kind(MessageDialogKind::Warning) |
| .buttons(MessageDialogButtons::Ok) |
| .blocking_show(); |
| } |
| } |
| |
| #[cfg(not(desktop))] |
| { |
| app.dialog() |
| .message("Updates are managed through your app store.") |
| .title("Check App Store") |
| .kind(MessageDialogKind::Info) |
| .buttons(MessageDialogButtons::Ok) |
| .blocking_show(); |
| } |
| |
| Ok(()) |
| } |
|
|
| fn toggle_search_window( |
| app: &tauri::AppHandle, |
| search_state: &SearchWindowState, |
| ) -> Result<(), Box<dyn std::error::Error>> { |
| println!("π === TOGGLE_SEARCH_WINDOW CALLED ==="); |
|
|
| let is_search_enabled = { |
| let guard = search_state.lock().unwrap(); |
| let value = *guard; |
| println!("π Current search window state from lock: {}", value); |
| value |
| }; |
|
|
| if !is_search_enabled { |
| println!("β Search window disabled, showing main window instead"); |
| |
| if let Some(main_window) = app.get_webview_window("main") { |
| main_window.show()?; |
| main_window.set_focus()?; |
| } |
| return Ok(()); |
| } |
|
|
| println!("β
Search window enabled, proceeding with search toggle"); |
| |
| let search_window_label = "search"; |
|
|
| println!("π Looking for existing search window..."); |
| if let Some(window) = app.get_webview_window(search_window_label) { |
| println!("π Found existing search window"); |
| if window.is_visible()? { |
| println!("π Search window is visible, hiding it"); |
| |
| let _ = window.emit("search-window-open", false); |
| window.hide()?; |
| } else { |
| println!("π Search window is hidden, showing it"); |
| |
| window.set_always_on_top(true)?; |
| position_window_on_current_monitor(app, &window)?; |
| window.show()?; |
| window.set_focus()?; |
|
|
| |
| let _ = window.emit("search-window-open", true); |
| } |
| } else { |
| println!("π Search window doesn't exist, creating it now..."); |
| |
| let app_url = get_app_url(); |
| let app_clone = app.clone(); |
|
|
| |
| tauri::async_runtime::block_on(async move { |
| if let Ok(_) = create_preloaded_search_window(&app_clone, &app_url).await { |
| println!("β
Search window created successfully via block_on"); |
| |
| if let Some(window) = app_clone.get_webview_window("search") { |
| let _ = window.set_always_on_top(true); |
| let _ = position_window_on_current_monitor(&app_clone, &window); |
| let _ = window.show(); |
| let _ = window.set_focus(); |
| let _ = window.emit("search-window-open", true); |
| println!("β
Search window shown successfully"); |
| } else { |
| println!("β Search window not found after creation"); |
| } |
| } else { |
| println!("β Failed to create search window"); |
| } |
| }); |
| } |
|
|
| Ok(()) |
| } |
|
|
| fn position_window_on_current_monitor( |
| app: &tauri::AppHandle, |
| window: &tauri::WebviewWindow, |
| ) -> Result<(), Box<dyn std::error::Error>> { |
| |
| if let Ok(cursor_position) = app.cursor_position() { |
| |
| if let Ok(monitors) = app.available_monitors() { |
| |
| let current_monitor = monitors.iter().find(|monitor| { |
| let pos = monitor.position(); |
| let size = monitor.size(); |
| cursor_position.x >= pos.x as f64 |
| && cursor_position.x < (pos.x + size.width as i32) as f64 |
| && cursor_position.y >= pos.y as f64 |
| && cursor_position.y < (pos.y + size.height as i32) as f64 |
| }); |
|
|
| if let Some(monitor) = current_monitor { |
| let monitor_size = monitor.size(); |
| let monitor_position = monitor.position(); |
|
|
| |
| let window_size = window.outer_size().unwrap_or(tauri::PhysicalSize { |
| width: 720, |
| height: 450, |
| }); |
|
|
| |
| let center_x = monitor_position.x + (monitor_size.width as i32 / 2) |
| - (window_size.width as i32 / 2); |
| let center_y = monitor_position.y + (monitor_size.height as i32 / 2) |
| - (window_size.height as i32 / 2); |
|
|
| |
| let center_y = center_y + 15; |
|
|
| window.set_position(Position::Physical(PhysicalPosition { |
| x: center_x, |
| y: center_y, |
| }))?; |
|
|
| return Ok(()); |
| } |
| } |
| } |
|
|
| |
| window.center()?; |
| Ok(()) |
| } |
|
|
| async fn create_preloaded_search_window( |
| app: &tauri::AppHandle, |
| app_url: &str, |
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| let search_window_label = "search"; |
| let search_url = format!("{}/desktop/search", app_url); |
|
|
| let mut search_builder = WebviewWindowBuilder::new( |
| app, |
| search_window_label, |
| WebviewUrl::External(tauri::Url::parse(&search_url)?), |
| ) |
| .title("Midday Search") |
| .inner_size(720.0, 450.0) |
| .min_inner_size(720.0, 450.0) |
| .resizable(false) |
| .user_agent("Mozilla/5.0 (compatible; Midday Desktop App)") |
| .transparent(true) |
| .decorations(false) |
| .visible(false) |
| .on_download(|_window, _event| { |
| println!("Search window download triggered!"); |
| |
| true |
| }); |
|
|
| |
| search_builder = search_builder |
| .hidden_title(true) |
| .title_bar_style(TitleBarStyle::Overlay); |
|
|
| let search_window = search_builder.shadow(false).build()?; |
|
|
| |
| search_window.center()?; |
|
|
| |
|
|
| |
| let window_clone = search_window.clone(); |
| search_window.on_window_event(move |event| { |
| match event { |
| |
| tauri::WindowEvent::Focused(false) => { |
| |
| let _ = window_clone.emit("search-window-open", false); |
| |
| let _ = window_clone.set_always_on_top(false); |
| let _ = window_clone.hide(); |
| } |
| |
| tauri::WindowEvent::Resized(_) | tauri::WindowEvent::Moved(_) => { |
| |
| if let Ok(false) = window_clone.is_focused() { |
| let _ = window_clone.emit("search-window-open", false); |
| let _ = window_clone.set_always_on_top(false); |
| let _ = window_clone.hide(); |
| } |
| } |
| _ => {} |
| } |
| }); |
|
|
| Ok(()) |
| } |
|
|
| fn get_app_url() -> String { |
| |
| let env = env::var("MIDDAY_ENV") |
| .unwrap_or_else(|_| { |
| option_env!("MIDDAY_ENV") |
| .unwrap_or("development") |
| .to_string() |
| }); |
|
|
| println!("π Environment detected: {}", env); |
|
|
| match env.as_str() { |
| "development" | "dev" => { |
| let url = "http://localhost:3001".to_string(); |
| println!("π Using development URL: {}", url); |
| url |
| }, |
| "staging" => { |
| let url = "https://beta.midday.ai".to_string(); |
| println!("π Using staging URL: {}", url); |
| url |
| }, |
| "production" | "prod" => { |
| let url = "https://app.midday.ai".to_string(); |
| println!("π Using production URL: {}", url); |
| url |
| }, |
| _ => { |
| eprintln!("Unknown environment: {}, defaulting to development", env); |
| let url = "http://localhost:3001".to_string(); |
| println!("π Using fallback development URL: {}", url); |
| url |
| } |
| } |
| } |
|
|
| fn is_external_url(url: &str, app_url: &str) -> bool { |
| |
| if let (Ok(target_url), Ok(base_url)) = (tauri::Url::parse(url), tauri::Url::parse(app_url)) { |
| |
| let is_http_scheme = target_url.scheme() == "http" || target_url.scheme() == "https"; |
|
|
| |
| let is_different_host = target_url.host() != base_url.host(); |
|
|
| return is_http_scheme && is_different_host; |
| } |
| false |
| } |
|
|
| fn handle_deep_link_event(app_handle: &tauri::AppHandle, urls: Vec<String>) { |
| for url in &urls { |
| |
| if let Some(idx) = url.find("://") { |
| let scheme = &url[..idx]; |
| if !scheme.contains("midday") { |
| continue; |
| } |
| let path = &url[idx + 3..]; |
|
|
| |
| let clean_path = path.trim_start_matches('/'); |
|
|
| |
| if let Some(window) = app_handle.get_webview_window("main") { |
| |
| if let Ok(_) = window.emit("deep-link-navigate", clean_path) { |
| |
| let _ = window.show(); |
| let _ = window.set_focus(); |
| } |
| } |
| } |
| } |
| } |
|
|
| #[cfg_attr(mobile, tauri::mobile_entry_point)] |
| pub fn run() { |
| let app_url = get_app_url(); |
|
|
| tauri::Builder::default() |
| .plugin(tauri_plugin_opener::init()) |
| .plugin(tauri_plugin_deep_link::init()) |
| .plugin(tauri_plugin_dialog::init()) |
| .plugin(tauri_plugin_process::init()) |
| .plugin(tauri_plugin_upload::init()) |
| .plugin(tauri_plugin_fs::init()) |
| .invoke_handler(tauri::generate_handler![show_window, check_for_updates]) |
| .setup(move |app| { |
| |
| #[cfg(desktop)] |
| app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; |
|
|
| |
| #[cfg(desktop)] |
| { |
| let app_handle_for_updates = app.handle().clone(); |
| tauri::async_runtime::spawn(async move { |
| |
| tokio::time::sleep(std::time::Duration::from_secs(5)).await; |
| println!("Running startup update check..."); |
| silent_update_check(app_handle_for_updates.clone()).await; |
|
|
| |
| let mut interval = tokio::time::interval(std::time::Duration::from_secs(4 * 60 * 60)); |
| interval.tick().await; |
| loop { |
| interval.tick().await; |
| println!("Running periodic update check..."); |
| silent_update_check(app_handle_for_updates.clone()).await; |
| } |
| }); |
| } |
|
|
| let app_url_clone = app_url.clone(); |
| let app_handle = app.handle().clone(); |
|
|
| |
| let search_state: SearchWindowState = Arc::new(Mutex::new(false)); |
| |
| |
| app.manage(search_state.clone()); |
|
|
| |
| let app_handle_for_deep_links = app_handle.clone(); |
| let app_handle_for_navigation = app_handle.clone(); |
| |
| |
|
|
| |
| { |
| use tauri_plugin_global_shortcut::{ |
| Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState, |
| }; |
|
|
| let search_shortcut = |
| Shortcut::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyK); |
|
|
| if let Ok(_) = app.handle().plugin( |
| tauri_plugin_global_shortcut::Builder::new() |
| .with_handler(move |app_handle, shortcut, event| { |
| if shortcut == &search_shortcut |
| && event.state() == ShortcutState::Pressed |
| { |
| println!("π Global shortcut triggered - checking search state via managed state"); |
| |
| if let Some(managed_search_state) = app_handle.try_state::<SearchWindowState>() { |
| let current_search_state = *managed_search_state.lock().unwrap(); |
| println!("π Shortcut: Search state from managed state: {}", current_search_state); |
| |
| |
| let result = toggle_search_window(app_handle, &managed_search_state); |
| match result { |
| Ok(_) => println!("π Shortcut: toggle_search_window returned Ok"), |
| Err(e) => println!("π Shortcut: toggle_search_window returned Err: {}", e) |
| } |
| } else { |
| println!("β Failed to get managed search state for shortcut"); |
| } |
| } |
| }) |
| .build(), |
| ) { |
| let _ = app.global_shortcut().register(search_shortcut); |
| } |
| } |
|
|
| |
| |
| |
| #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] |
| { |
| match app_handle.deep_link().register_all() { |
| Ok(_) => println!("π Deep link schemes registered successfully"), |
| Err(e) => eprintln!("π Failed to register deep link schemes: {}", e), |
| } |
| } |
|
|
| |
| if let Ok(urls) = app_handle.deep_link().get_current() { |
| println!("π Current deep link URLs on launch: {:?}", urls); |
| } |
|
|
| |
| app_handle.deep_link().on_open_url(move |event| { |
| let url_strings: Vec<String> = |
| event.urls().iter().map(|url| url.to_string()).collect(); |
| println!("π Deep link received: {:?}", url_strings); |
| handle_deep_link_event(&app_handle_for_deep_links, url_strings); |
| }); |
|
|
| let win_builder = WebviewWindowBuilder::new( |
| app, |
| "main", |
| WebviewUrl::External(tauri::Url::parse(&app_url).unwrap()), |
| ) |
| .title("Midday") |
| .inner_size(1450.0, 910.0) |
| .min_inner_size(1450.0, 910.0) |
| .user_agent("Mozilla/5.0 (compatible; Midday Desktop App)") |
| .decorations(false) |
| .visible(false) |
| .transparent(true) |
| .shadow(true) |
| .hidden_title(true) |
| .title_bar_style(TitleBarStyle::Overlay) |
| .disable_drag_drop_handler() |
| .on_download(|_window, _event| { |
| println!("Download triggered!"); |
| |
| true |
| }) |
| .on_navigation(move |url| { |
| let url_str = url.as_str(); |
|
|
| |
| if is_external_url(url_str, &app_url_clone) { |
| |
| let url_string = url_str.to_string(); |
| let app_handle_clone = app_handle_for_navigation.clone(); |
|
|
| |
| tauri::async_runtime::spawn(async move { |
| let _ = tauri_plugin_opener::OpenerExt::opener(&app_handle_clone) |
| .open_url(url_string, None::<String>); |
| }); |
|
|
| |
| return false; |
| } |
|
|
| |
| true |
| }); |
|
|
| let window = win_builder.build().unwrap(); |
|
|
| |
| let search_state_for_events = search_state.clone(); |
| let app_handle_for_events = app_handle.clone(); |
| window.listen("search-window-enabled", move |event| { |
| if let Ok(enabled) = serde_json::from_str::<bool>(&event.payload()) { |
| println!("π Event received: search-window-enabled = {}", enabled); |
| *search_state_for_events.lock().unwrap() = enabled; |
| println!("π Search window state updated to {}", enabled); |
| |
| |
| if !enabled { |
| println!("π Search disabled, cleaning up search window"); |
| if let Some(search_window) = app_handle_for_events.get_webview_window("search") { |
| let _ = search_window.close(); |
| println!("π Search window closed and cleaned up"); |
| } |
| } |
| } |
| }); |
|
|
| |
| let app_handle_for_close = app_handle.clone(); |
| window.listen("search-window-close-requested", move |_event| { |
| println!("π Event received: search-window-close-requested"); |
| if let Some(search_window) = app_handle_for_close.get_webview_window("search") { |
| let _ = search_window.emit("search-window-open", false); |
| let _ = search_window.set_always_on_top(false); |
| let _ = search_window.hide(); |
| println!("π Search window closed via close request"); |
| } |
| }); |
|
|
| |
| let window_clone = window.clone(); |
| tauri::async_runtime::spawn(async move { |
| std::thread::sleep(std::time::Duration::from_secs(2)); |
|
|
| |
| if let Ok(is_visible) = window_clone.is_visible() { |
| if !is_visible { |
| let _ = window_clone.show(); |
| let _ = window_clone.set_focus(); |
| } |
| } |
| }); |
|
|
| |
| |
|
|
| |
| let app_menu = Menu::default(app.handle())?; |
| app.set_menu(app_menu)?; |
|
|
| |
| |
| let tray_icon = { |
| let icon_bytes = include_bytes!("../icons/tray-icon.png"); |
| let img = image::load_from_memory(icon_bytes).map_err(|e| format!("Failed to load tray icon: {}", e))?; |
| let rgba = img.to_rgba8(); |
| let (width, height) = rgba.dimensions(); |
| Image::new_owned(rgba.into_raw(), width, height) |
| }; |
|
|
| |
| let check_updates_item = MenuItem::with_id(app, "check_updates", "Check for Updates...", true, None::<&str>)?; |
| let tray_menu = Menu::with_items(app, &[&check_updates_item])?; |
|
|
| let _tray = TrayIconBuilder::new() |
| .icon(tray_icon) |
| .menu(&tray_menu) |
| .show_menu_on_left_click(false) |
| .on_menu_event(|app, event| { |
| println!("π§ Tray menu event triggered: {:?}", event.id); |
| if event.id == "check_updates" { |
| println!("π§ Calling check_for_updates..."); |
| let app_handle = app.clone(); |
| tauri::async_runtime::spawn(async move { |
| let _ = check_for_updates(app_handle).await; |
| }); |
| } |
| }) |
| .on_tray_icon_event(move |tray, event| { |
| match event { |
| |
| TrayIconEvent::Click { |
| button: MouseButton::Left, |
| button_state: MouseButtonState::Up, |
| .. |
| } => { |
| let app_handle = tray.app_handle(); |
| if let Some(managed_search_state) = app_handle.try_state::<SearchWindowState>() { |
| let _ = toggle_search_window(app_handle, &managed_search_state); |
| } |
| }, |
| _ => {} |
| } |
| }) |
| .build(app)?; |
|
|
| Ok(()) |
| }) |
| .build(tauri::generate_context!()) |
| .expect("error while building tauri app") |
| .run(|app_handle, event| match event { |
| tauri::RunEvent::Reopen { .. } => { |
| if let Some(main_window) = app_handle.get_webview_window("main") { |
| let _ = main_window.show(); |
| let _ = main_window.set_focus(); |
| } |
| } |
| tauri::RunEvent::ExitRequested { api, .. } => { |
| |
| api.prevent_exit(); |
| |
| |
| if let Some(main_window) = app_handle.get_webview_window("main") { |
| let _ = main_window.hide(); |
| } |
| if let Some(search_window) = app_handle.get_webview_window("search") { |
| let _ = search_window.hide(); |
| } |
| } |
| _ => {} |
| }); |
| } |
|
|