diff --git a/desktop/angular/src/app/layout/navigation/navigation.html b/desktop/angular/src/app/layout/navigation/navigation.html index 09b8e5f9..ae961f6c 100644 --- a/desktop/angular/src/app/layout/navigation/navigation.html +++ b/desktop/angular/src/app/layout/navigation/navigation.html @@ -220,10 +220,10 @@
- Paused: {{ pauseInfo }} + {{ pauseInfo }} - Auto-resume at: {{ pauseInfoTillTime }} + Auto-resume at {{ pauseInfoTillTime }}

diff --git a/desktop/angular/src/app/layout/navigation/navigation.ts b/desktop/angular/src/app/layout/navigation/navigation.ts index 48195e5d..1cff5acb 100644 --- a/desktop/angular/src/app/layout/navigation/navigation.ts +++ b/desktop/angular/src/app/layout/navigation/navigation.ts @@ -42,11 +42,11 @@ export class NavigationComponent implements OnInit { get isPausedSPN(): boolean { return this.pauseState?.SPN===true; } get pauseInfo(): string { if (this.pauseState?.Interception===true && this.pauseState?.SPN===true) - return 'Portmaster and SPN'; + return 'Portmaster and SPN are paused'; else if (this.pauseState?.Interception===true) - return 'Portmaster'; + return 'Portmaster is paused'; else if (this.pauseState?.SPN===true) - return 'SPN'; + return 'SPN is paused'; return ''; } get pauseInfoTillTime(): string { diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 2f5e7e06..6d1c7727 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -859,8 +859,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -3949,6 +3951,7 @@ version = "2.0.25" dependencies = [ "assert_matches", "cached", + "chrono", "clap_lex", "ctor", "dark-light", diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index fb562e22..b7e9a6d6 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -52,6 +52,7 @@ reqwest = { version = "0.12", features = ["cookies", "json"] } rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] } open = "5.1.3" +chrono = { version = "0.4", features = ["serde"] } dark-light = { path = "../rust-dark-light" } diff --git a/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs b/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs index afb24337..c4e0a47a 100644 --- a/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs +++ b/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs @@ -45,7 +45,7 @@ pub struct WorstState { pub state: State, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SystemStatus { #[serde(rename = "Modules")] pub modules: Vec, @@ -54,4 +54,28 @@ pub struct SystemStatus { // add more fields when needed // ... +} + +// PauseInfo represents pause status data from "control:paused" state in Control module +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PauseInfo { + #[serde(rename = "Interception")] + pub interception: bool, + #[serde(rename = "SPN")] + pub spn: bool, + #[serde(rename = "TillTime")] + pub till_time: String, // time.Time serialized as string by GoLang +} + + +impl SystemStatus { + pub fn get_module_state(&self, module_name: &str, state_id: &str) -> Option<&State> { + if let Some(module) = self.modules.iter().find(|m| m.module == module_name) { + if let Some(states) = &module.states { + return states.iter().find(|s| s.id == state_id); + } + } + None + } + } \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 106793bf..1d7a88b7 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use std::sync::atomic::AtomicBool; use std::sync::RwLock; use std::{sync::atomic::Ordering}; +use chrono::{DateTime}; use log::{debug, error}; use tauri::{ @@ -66,6 +67,8 @@ const PAUSE_PM_5_KEY: &str = "pause_pm_5"; const PAUSE_PM_15_KEY: &str = "pause_pm_15"; const PAUSE_PM_60_KEY: &str = "pause_pm_60"; const RESUME_KEY: &str = "resume_all"; +const PAUSE_INFO_KEY: &str = "pause_info"; +const PAUSE_INFO_TIME_KEY: &str = "pause_info_time"; // Icons @@ -133,6 +136,7 @@ fn build_tray_menu( app: &tauri::AppHandle, status: &str, spn_status_text: &str, + pause_info: &system_status_types::PauseInfo, ) -> core::result::Result> { load_theme(app); @@ -140,22 +144,46 @@ fn build_tray_menu( let exit_ui_btn = MenuItemBuilder::with_id(EXIT_UI_KEY, "Exit UI").build(app)?; let shutdown_btn = MenuItemBuilder::with_id(SHUTDOWN_KEY, "Shut Down Portmaster").build(app)?; - let global_status = MenuItemBuilder::with_id(GLOBAL_STATUS_KEY, format!("Status: {}", status)) + // Global status + let global_status_text = if pause_info.interception { + format!("Status: {} (PAUSED)", status) + } else { + format!("Status: {}", status) + }; + let global_status = MenuItemBuilder::with_id(GLOBAL_STATUS_KEY, global_status_text) .enabled(false) .build(app) .unwrap(); - // Setup SPN status - let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text)) - .enabled(false) - .build(app) - .unwrap(); - + // Pause items + let (pause_status_item, pause_status_time_item, resume_item) = if pause_info.interception || pause_info.spn { + let status_text = match (pause_info.interception, pause_info.spn) { + (true, true) => "Portmaster and SPN are paused", + (true, false) => "Portmaster is paused", + (false, true) => "SPN is paused", + _ => unreachable!(), // We already checked at least one is true + }; + let status_item = MenuItemBuilder::with_id(PAUSE_INFO_KEY, status_text).enabled(false).build(app)?; + + let formatted_time = DateTime::parse_from_rfc3339(&pause_info.till_time) + .map(|dt| dt.with_timezone(&chrono::Local).format("%H:%M:%S").to_string()) + .unwrap_or_else(|_| pause_info.till_time.clone()); + let time_item = MenuItemBuilder::with_id(PAUSE_INFO_TIME_KEY, format!("Auto-resume at {}", formatted_time)).enabled(false).build(app)?; + let resume_item = MenuItemBuilder::with_id(RESUME_KEY, "Resume now").build(app)?; + (Some(status_item), Some(time_item), Some(resume_item)) + } else { + (None, None, None) + }; + // Setup SPN button let spn_button_text = match spn_status_text { "disabled" => "Enable SPN", _ => "Disable SPN", - }; + }; + let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text)) + .enabled(false) + .build(app) + .unwrap(); let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, spn_button_text) .build(app) .unwrap(); @@ -176,10 +204,10 @@ fn build_tray_menu( // Setup Pause/Resume menu items - let disabled_spn = spn_status_text == "disabled"; - let pause_spn_5min_item = MenuItemBuilder::with_id(PAUSE_SPN_5_KEY, "Pause SPN for 5 minutes").enabled(!disabled_spn).build(app)?; - let pause_spn_15min_item = MenuItemBuilder::with_id(PAUSE_SPN_15_KEY, "Pause SPN for 15 minutes").enabled(!disabled_spn).build(app)?; - let pause_spn_1hour_item = MenuItemBuilder::with_id(PAUSE_SPN_60_KEY, "Pause SPN for 1 hour").enabled(!disabled_spn).build(app)?; + let disabled_spn_pause = (spn_status_text == "disabled" && !pause_info.spn) || pause_info.interception; + let pause_spn_5min_item = MenuItemBuilder::with_id(PAUSE_SPN_5_KEY, "Pause SPN for 5 minutes").enabled(!disabled_spn_pause).build(app)?; + let pause_spn_15min_item = MenuItemBuilder::with_id(PAUSE_SPN_15_KEY, "Pause SPN for 15 minutes").enabled(!disabled_spn_pause).build(app)?; + let pause_spn_1hour_item = MenuItemBuilder::with_id(PAUSE_SPN_60_KEY, "Pause SPN for 1 hour").enabled(!disabled_spn_pause).build(app)?; let pause_pm_5min_item = MenuItemBuilder::with_id(PAUSE_PM_5_KEY, "Pause for 5 minutes").build(app)?; let pause_pm_15min_item = MenuItemBuilder::with_id(PAUSE_PM_15_KEY, "Pause for 15 minutes").build(app)?; @@ -197,8 +225,6 @@ fn build_tray_menu( ]) .build()?; - let resume_item = MenuItemBuilder::with_id(RESUME_KEY, "Resume now").build(app)?; - /* DEV MENU let force_show_window = MenuItemBuilder::with_id(FORCE_SHOW_KEY, "Force Show UI").build(app)?; let reload_btn = MenuItemBuilder::with_id(RELOAD_KEY, "Reload User Interface").build(app)?; @@ -206,25 +232,44 @@ fn build_tray_menu( .items(&[&reload_btn, &force_show_window]) .build()?; */ + + // Assemble menu items + let s = PredefinedMenuItem::separator(app)?; + let mut items: Vec<&dyn tauri::menu::IsMenuItem> = Vec::new(); + + + items.push(&global_status); + items.push(&s); + + if let Some(ref pause_status_item) = pause_status_item { + items.push(pause_status_item); + } + if let Some(ref pause_status_time_item) = pause_status_time_item { + items.push(pause_status_time_item); + } + if let Some(ref resume_item) = resume_item { + items.push(resume_item); + } + items.push(&pause_menu); + items.push(&s); + + items.push(&spn_status); + items.push(&spn_button); + items.push(&s); + + items.push(&theme_menu); + items.push(&s); + + items.push(&open_btn); + items.push(&s); + + items.push(&exit_ui_btn); + items.push(&shutdown_btn); + //items.push(&developer_menu); + let menu = MenuBuilder::with_id(app, PM_TRAY_MENU_ID) - .items(&[ - &open_btn, - &PredefinedMenuItem::separator(app)?, - &global_status, - &PredefinedMenuItem::separator(app)?, - &pause_menu, - &resume_item, - &PredefinedMenuItem::separator(app)?, - &spn_status, - &spn_button, - &PredefinedMenuItem::separator(app)?, - &theme_menu, - &PredefinedMenuItem::separator(app)?, - &exit_ui_btn, - &shutdown_btn, - //&developer_menu, - ]) + .items(&items) .build()?; return Ok(menu); @@ -233,7 +278,7 @@ fn build_tray_menu( pub fn setup_tray_menu( app: &mut tauri::App, ) -> core::result::Result> { - let menu = build_tray_menu(app.handle(), "Secured", "disabled")?; + let menu = build_tray_menu(app.handle(), "Secured", "disabled", &system_status_types::PauseInfo::default())?; let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID) .icon(Image::from_bytes(get_red_icon()).unwrap()) @@ -325,15 +370,10 @@ pub fn setup_tray_menu( pub fn update_icon(icon: AppIcon, system_status: SystemStatus, spn_status: String) { // Extract the worst state type - let worst_state_type = match system_status.worst_state { - Some(ws) => { - match ws.state.state_type { - Some(s) => s, - None => system_status_types::StateType::Undefined - } - } - None => system_status_types::StateType::Undefined - }; + let worst_state_type = system_status.worst_state + .as_ref() + .and_then(|ws| ws.state.state_type.clone()) + .unwrap_or(system_status_types::StateType::Undefined); // Determine status and icon color in a single match expression let (status, icon_color) = match worst_state_type { @@ -348,7 +388,15 @@ pub fn update_icon(icon: AppIcon, system_status: SystemStatus, spn_status: Strin } }; - if let Ok(menu) = build_tray_menu(icon.app_handle(), status.as_ref(), spn_status.as_str()) { + // Extract pause info from system status + let pause_info = system_status + .get_module_state("Control", "control:paused") + .and_then(|state| state.data.as_ref()) + .and_then(|data| serde_json::from_value::(data.clone()).ok()) + .unwrap_or_default(); + + // Rebuild and set the tray menu + if let Ok(menu) = build_tray_menu(icon.app_handle(), status, spn_status.as_str(), &pause_info) { if let Err(err) = icon.set_menu(Some(menu)) { error!("failed to set menu on tray icon: {}", err.to_string()); } @@ -432,11 +480,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_icon_color(&icon, IconColor::Blue); - let mut system_status = SystemStatus { - modules: Vec::new(), - worst_state: None, - }; - + let mut system_status = SystemStatus::default(); let mut spn_status: String = "".to_string(); loop {