| use tauri::{ |
| image::Image, |
| menu::{Menu, MenuItem, PredefinedMenuItem}, |
| tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, |
| Manager, Emitter, Listener, |
| }; |
| use crate::modules; |
|
|
| pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { |
| |
| let config = modules::load_app_config().unwrap_or_default(); |
| let texts = modules::i18n::get_tray_texts(&config.language); |
| |
| |
| let icon_bytes = include_bytes!("../../icons/tray-icon.png"); |
| let img = image::load_from_memory(icon_bytes) |
| .map_err(|e| tauri::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))? |
| .to_rgba8(); |
| let (width, height) = img.dimensions(); |
| let icon = Image::new_owned(img.into_raw(), width, height); |
|
|
| |
| |
| let loading_text = format!("{}: ...", texts.current); |
| let quota_text = format!("{}: --", texts.quota); |
| let info_user = MenuItem::with_id(app, "info_user", &loading_text, false, None::<&str>)?; |
| let info_quota = MenuItem::with_id(app, "info_quota", "a_text, false, None::<&str>)?; |
|
|
| |
| let switch_next = MenuItem::with_id(app, "switch_next", &texts.switch_next, true, None::<&str>)?; |
| let refresh_curr = MenuItem::with_id(app, "refresh_curr", &texts.refresh_current, true, None::<&str>)?; |
| |
| |
| let show_i = MenuItem::with_id(app, "show", &texts.show_window, true, None::<&str>)?; |
| let quit_i = MenuItem::with_id(app, "quit", &texts.quit, true, None::<&str>)?; |
| |
| let sep1 = PredefinedMenuItem::separator(app)?; |
| let sep2 = PredefinedMenuItem::separator(app)?; |
| let sep3 = PredefinedMenuItem::separator(app)?; |
|
|
| |
| let menu = Menu::with_items(app, &[ |
| &info_user, |
| &info_quota, |
| &sep1, |
| &switch_next, |
| &refresh_curr, |
| &sep2, |
| &show_i, |
| &sep3, |
| &quit_i, |
| ])?; |
|
|
| |
| let _ = TrayIconBuilder::with_id("main") |
| .menu(&menu) |
| .show_menu_on_left_click(false) |
| .icon(icon) |
| .on_menu_event(move |app, event| { |
| let app_handle = app.clone(); |
| match event.id().as_ref() { |
| "show" => { |
| if let Some(window) = app.get_webview_window("main") { |
| let _ = window.show(); |
| let _ = window.set_focus(); |
| #[cfg(target_os = "macos")] |
| app.set_activation_policy(tauri::ActivationPolicy::Regular).unwrap_or(()); |
| } |
| } |
| "quit" => { |
| |
| let state = app.state::<crate::commands::proxy::ProxyServiceState>(); |
| let admin_server = state.admin_server.clone(); |
| tauri::async_runtime::spawn(async move { |
| let mut lock = admin_server.write().await; |
| if let Some(admin) = lock.take() { |
| admin.axum_server.stop(); |
| tokio::time::sleep(std::time::Duration::from_millis(100)).await; |
| } |
| }); |
| |
| std::thread::sleep(std::time::Duration::from_millis(200)); |
| app.exit(0); |
| } |
| "refresh_curr" => { |
| |
| tauri::async_runtime::spawn(async move { |
| if let Ok(Some(account_id)) = modules::get_current_account_id() { |
| |
| let _ = app_handle.emit("tray://refresh-current", ()); |
| |
| |
| if let Ok(mut account) = modules::load_account(&account_id) { |
| |
| match modules::account::fetch_quota_with_retry(&mut account).await { |
| Ok(quota) => { |
| |
| let _ = modules::update_account_quota(&account.id, quota); |
| |
| update_tray_menus(&app_handle); |
| }, |
| Err(e) => { |
| |
| modules::logger::log_error(&format!("Tray refresh failed: {}", e)); |
| } |
| } |
| } |
| } |
| }); |
| } |
| "switch_next" => { |
| tauri::async_runtime::spawn(async move { |
| |
| if let Ok(accounts) = modules::list_accounts() { |
| if accounts.is_empty() { return; } |
| |
| let current_id = modules::get_current_account_id().unwrap_or(None); |
| let next_account = if let Some(curr) = current_id { |
| let idx = accounts.iter().position(|a| a.id == curr).unwrap_or(0); |
| let next_idx = (idx + 1) % accounts.len(); |
| &accounts[next_idx] |
| } else { |
| &accounts[0] |
| }; |
| |
| |
| let integration = crate::modules::integration::DesktopIntegration { |
| app_handle: app_handle.clone(), |
| }; |
| if let Ok(_) = modules::switch_account(&next_account.id, &integration).await { |
| |
| let _ = app_handle.emit("tray://account-switched", next_account.id.clone()); |
| |
| update_tray_menus(&app_handle); |
| } |
| } |
| }); |
| } |
| _ => {} |
| } |
| }) |
| .on_tray_icon_event(|tray, event| { |
| if let TrayIconEvent::Click { |
| button: MouseButton::Left, |
| .. |
| } = event |
| { |
| let app = tray.app_handle(); |
| if let Some(window) = app.get_webview_window("main") { |
| let _ = window.show(); |
| let _ = window.set_focus(); |
| #[cfg(target_os = "macos")] |
| app.set_activation_policy(tauri::ActivationPolicy::Regular).unwrap_or(()); |
| } |
| } |
| }) |
| .build(app)?; |
|
|
| |
| let handle = app.clone(); |
| tauri::async_runtime::spawn(async move { |
| update_tray_menus(&handle); |
| }); |
|
|
| |
| let handle = app.clone(); |
| app.listen("config://updated", move |_event| { |
| modules::logger::log_info("Configuration updated, refreshing tray menu"); |
| update_tray_menus(&handle); |
| }); |
|
|
| Ok(()) |
| } |
|
|
| |
| pub fn update_tray_menus(app: &tauri::AppHandle) { |
| let app_clone = app.clone(); |
| tauri::async_runtime::spawn(async move { |
| |
| let config = modules::load_app_config().unwrap_or_default(); |
| let texts = modules::i18n::get_tray_texts(&config.language); |
| |
| |
| let current = modules::get_current_account_id().unwrap_or(None); |
| |
| let mut menu_lines = Vec::new(); |
| let mut user_text = format!("{}: {}", texts.current, texts.no_account); |
|
|
| if let Some(id) = current { |
| if let Ok(account) = modules::load_account(&id) { |
| user_text = format!("{}: {}", texts.current, account.email); |
| |
| if let Some(q) = account.quota { |
| if q.is_forbidden { |
| menu_lines.push(format!("🚫 {}", texts.forbidden)); |
| } else { |
| |
| let mut gemini_high = 0; |
| let mut gemini_image = 0; |
| let mut claude = 0; |
| |
| |
| for m in q.models { |
| let name = m.name.to_lowercase(); |
| if name == "gemini-3.1-pro-high" || name == "gemini-3-pro-high" { gemini_high = m.percentage; } |
| if name == "gemini-3-pro-image" { gemini_image = m.percentage; } |
| if name == "claude-sonnet-4-6" || name == "claude-sonnet-4-5" { claude = m.percentage; } |
| } |
| |
| menu_lines.push(format!("Gemini High: {}%", gemini_high)); |
| menu_lines.push(format!("Gemini Image: {}%", gemini_image)); |
| menu_lines.push(format!("Claude 4.5: {}%", claude)); |
| } |
| } else { |
| menu_lines.push(texts.unknown_quota.clone()); |
| } |
| } else { |
| user_text = format!("{}: Error", texts.current); |
| menu_lines.push(format!("{}: --", texts.quota)); |
| } |
| } else { |
| menu_lines.push(texts.unknown_quota.clone()); |
| }; |
|
|
| |
| let info_user = MenuItem::with_id(&app_clone, "info_user", &user_text, false, None::<&str>); |
| |
| |
| let mut quota_items = Vec::new(); |
| for (i, line) in menu_lines.iter().enumerate() { |
| let item = MenuItem::with_id(&app_clone, format!("info_quota_{}", i), line, false, None::<&str>); |
| if let Ok(item) = item { |
| quota_items.push(item); |
| } |
| } |
| |
| let switch_next = MenuItem::with_id(&app_clone, "switch_next", &texts.switch_next, true, None::<&str>); |
| let refresh_curr = MenuItem::with_id(&app_clone, "refresh_curr", &texts.refresh_current, true, None::<&str>); |
| |
| let show_i = MenuItem::with_id(&app_clone, "show", &texts.show_window, true, None::<&str>); |
| let quit_i = MenuItem::with_id(&app_clone, "quit", &texts.quit, true, None::<&str>); |
| |
| if let (Ok(i_u), Ok(s_n), Ok(r_c), Ok(s), Ok(q)) = (info_user, switch_next, refresh_curr, show_i, quit_i) { |
| let sep1 = PredefinedMenuItem::separator(&app_clone).ok(); |
| let sep2 = PredefinedMenuItem::separator(&app_clone).ok(); |
| let sep3 = PredefinedMenuItem::separator(&app_clone).ok(); |
| |
| let mut items: Vec<&dyn tauri::menu::IsMenuItem<tauri::Wry>> = vec![&i_u]; |
| |
| for item in "a_items { |
| items.push(item); |
| } |
| |
| if let Some(ref s) = sep1 { items.push(s); } |
| items.push(&s_n); |
| items.push(&r_c); |
| if let Some(ref s) = sep2 { items.push(s); } |
| items.push(&s); |
| if let Some(ref s) = sep3 { items.push(s); } |
| items.push(&q); |
| |
| if let Ok(menu) = Menu::with_items(&app_clone, &items) { |
| if let Some(tray) = app_clone.tray_by_id("main") { |
| let _ = tray.set_menu(Some(menu)); |
| } |
| } |
| } |
| }); |
| } |
|
|