feat(UI): enhance pause info display in system tray menu and reorganize menu item positions
https://github.com/safing/portmaster/issues/2050
This commit is contained in:
@@ -220,10 +220,10 @@
|
||||
<div *ngIf="isPaused">
|
||||
<div class="flex flex-col p-4 text-xxs">
|
||||
<span class="text-secondary">
|
||||
Paused: <span class="text-primary">{{ pauseInfo }} </span>
|
||||
<span class="text-secondary">{{ pauseInfo }} </span>
|
||||
</span>
|
||||
<span class="text-secondary">
|
||||
Auto-resume at: <span class="text-primary">{{ pauseInfoTillTime }} </span>
|
||||
Auto-resume at <span class="text-secondary">{{ pauseInfoTillTime }} </span>
|
||||
</span>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
3
desktop/tauri/src-tauri/Cargo.lock
generated
3
desktop/tauri/src-tauri/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
@@ -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<StateUpdate>,
|
||||
@@ -55,3 +55,27 @@ 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ContextMenu, Box<dyn std::error::Error>> {
|
||||
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)?;
|
||||
@@ -207,24 +233,43 @@ fn build_tray_menu(
|
||||
.build()?;
|
||||
*/
|
||||
|
||||
// Assemble menu items
|
||||
let s = PredefinedMenuItem::separator(app)?;
|
||||
let mut items: Vec<&dyn tauri::menu::IsMenuItem<Wry>> = 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<AppIcon, Box<dyn std::error::Error>> {
|
||||
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::<system_status_types::PauseInfo>(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 {
|
||||
|
||||
Reference in New Issue
Block a user