diff --git a/ThingHK/ThingHK.csproj b/ThingHK/ThingHK.csproj index ad41190..21e6b43 100644 --- a/ThingHK/ThingHK.csproj +++ b/ThingHK/ThingHK.csproj @@ -25,7 +25,7 @@ - + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b449ec6..baabe8b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2392,6 +2392,20 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2", + "objc2-foundation", + "time", + "uuid", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -2529,6 +2543,20 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "notify-rust" +version = "4.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -3061,6 +3089,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -3165,6 +3202,35 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4313,6 +4379,25 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -4451,6 +4536,17 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-version", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -4495,6 +4591,7 @@ dependencies = [ "tauri-plugin-autostart", "tauri-plugin-dialog", "tauri-plugin-global-shortcut", + "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-snap-layout", "tokio", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b93b278..45007b6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ tauri-plugin-opener = "2" tauri-plugin-dialog = "2" tauri-plugin-snap-layout = "1" tauri-plugin-global-shortcut = "2" +tauri-plugin-notification = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" diff --git a/src-tauri/binaries/ThingHK.exe b/src-tauri/binaries/ThingHK.exe index 6b9e513..1f39336 100644 Binary files a/src-tauri/binaries/ThingHK.exe and b/src-tauri/binaries/ThingHK.exe differ diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index 704660f..ac05cbf 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -9,6 +9,7 @@ "main" ], "permissions": [ - "autostart:default" + "autostart:default", + "notification:default" ] } \ No newline at end of file diff --git a/src-tauri/capabilities/tray-menu.json b/src-tauri/capabilities/tray-menu.json new file mode 100644 index 0000000..6069ff0 --- /dev/null +++ b/src-tauri/capabilities/tray-menu.json @@ -0,0 +1,20 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "tray-menu", + "description": "Capability for custom tray menu popup window", + "windows": ["tray-menu"], + "permissions": [ + "core:default", + "core:window:allow-hide", + "core:window:allow-show", + "core:window:allow-set-focus", + "core:window:allow-close", + "core:window:allow-set-theme", + "core:window:allow-set-effects", + "core:window:allow-set-background-color", + "core:window:allow-set-size", + "core:window:allow-set-position", + "core:event:allow-emit", + "core:event:allow-listen" + ] +} diff --git a/src-tauri/src/clipboard/popup.rs b/src-tauri/src/clipboard/popup.rs index df618f2..8a1573b 100644 --- a/src-tauri/src/clipboard/popup.rs +++ b/src-tauri/src/clipboard/popup.rs @@ -73,7 +73,7 @@ pub fn unregister_shortcut(app: &AppHandle) { /// 窗口不存在则创建(隐藏状态,等前端挂载后调用 show_window 显示)。 /// 窗口已存在则移动到鼠标位置并显示。 pub fn show_popup(app: &AppHandle) { - // 获取鼠标位置(屏幕坐标) + // 获取鼠标位置(物理像素,Per-Monitor DPI V2) let (mx, my) = match get_cursor_pos() { Some(p) => p, None => return, @@ -83,16 +83,30 @@ pub fn show_popup(app: &AppHandle) { let w = 380.0_f64; let h = 460.0_f64; - // 获取屏幕工作区以避免弹出位置超出屏幕 + // 获取屏幕工作区(物理像素,Per-Monitor DPI V2) let (screen_w, screen_h) = get_work_area().unwrap_or((1920.0, 1080.0)); - // 计算窗口位置:鼠标位置作为左上角,超出屏幕则调整 - let x = (mx as f64).min(screen_w - w).max(0.0); - let y = (my as f64).min(screen_h - h).max(0.0); + // 获取 scale factor,将窗口逻辑尺寸换算为物理像素用于边界裁剪 + let scale = app + .get_webview_window(POPUP_LABEL) + .as_ref() + .and_then(|w| w.scale_factor().ok()) + .or_else(|| { + app.get_webview_window("main") + .and_then(|w| w.scale_factor().ok()) + }) + .unwrap_or(1.0); + + let w_phys = (w * scale) as i32; + let h_phys = (h * scale) as i32; + + // 物理坐标 clamping + let x = mx.max(0).min(screen_w as i32 - w_phys); + let y = my.max(0).min(screen_h as i32 - h_phys); // 窗口已存在:移动 + 显示 + 请求焦点 if let Some(win) = app.get_webview_window(POPUP_LABEL) { - let _ = win.set_position(tauri::Position::Logical(tauri::LogicalPosition { + let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y, })); @@ -112,7 +126,7 @@ pub fn show_popup(app: &AppHandle) { ) .title("剪贴板") .inner_size(w, h) - .position(x, y) + .position(x as f64 / scale, y as f64 / scale) .decorations(false) .transparent(true) .shadow(true) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f63a9c7..830d405 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -51,6 +51,7 @@ use clipboard::{ clipboard_set_pinned, clipboard_show_popup, clipboard_show_window, clipboard_start, clipboard_status, clipboard_stop, clipboard_unregister_shortcut, }; +use tray_menu::{tray_menu_action, tray_menu_hide, tray_menu_show_window}; #[tauri::command] fn greet(name: &str) -> String { @@ -87,6 +88,7 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_notification::init()) .plugin( tauri_plugin_snap_layout::init() .button_id("titlebar-maximize") @@ -181,6 +183,9 @@ pub fn run() { clipboard_show_window, clipboard_hide_popup, clipboard_paste_to_target, + tray_menu_action, + tray_menu_hide, + tray_menu_show_window, snap_fix::fix_snap_background ]) .setup(|app| { diff --git a/src-tauri/src/tray_menu.rs b/src-tauri/src/tray_menu.rs index e607cb3..c01ca9f 100644 --- a/src-tauri/src/tray_menu.rs +++ b/src-tauri/src/tray_menu.rs @@ -1,46 +1,53 @@ -//! tray_menu.rs — 自定义系统托盘右键菜单 +//! tray_menu.rs — 自定义系统托盘菜单(WebviewWindow 弹窗) //! -//! 菜单结构: -//! 代理 ▸ -//! 开启代理 (启动mihomo + 择优选节点 + 系统代理 + Windows通知) -//! 关闭代理 -//! --- -//! 选择代理 ▸ -//! 刷新节点列表 -//! --- -//! 节点1 (120ms) -//! 节点2 (超时) -//! ... -//! --- -//! 切换 OSD -//! 重启 Kernel -//! --- -//! 新建下载 -//! --- -//! 常规设置 -//! 退出 - -use std::sync::{Mutex, OnceLock}; +//! 使用独立 WebviewWindow 替代原生菜单,实现与应用一致的视觉风格(Mica/Acrylic + 深色主题)。 +//! - 右键托盘:显示自定义菜单窗口(在鼠标位置弹出) +//! - 左键托盘:显示主窗口 +//! - 菜单窗口失焦:自动隐藏 +//! +//! 前端通过监听 `tray-menu-show` 事件获取菜单状态并渲染, +//! 点击菜单项时调用 `tray_menu_action` 命令执行对应操作。 +use serde::Serialize; +use std::sync::Mutex; +use std::time::{Duration, Instant}; use tauri::{ - AppHandle, Emitter, Manager, - menu::{IsMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu}, - tray::{MouseButton, TrayIconBuilder, TrayIconEvent, TrayIconId}, + AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder, + tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, + window::{Effect, EffectsBuilder}, }; +/// 记录窗口最后显示时间,用于失焦防抖(避免显示瞬间因焦点未稳定而被立即隐藏) +static LAST_SHOW_TIME: Mutex> = Mutex::new(None); + +use crate::clipboard::popup::{get_cursor_pos, get_work_area}; use crate::clipboard::ClipboardManager; use crate::download_engine::DownloadEngine; use crate::mihomo_manager::MihomoManager; use crate::monitor_kernel::MonitorKernel; use crate::process_manager::{ProcessManager, ProcessStatus}; -// ===== 全局状态:代理节点菜单项映射 ===== +/// 托盘菜单窗口标签 +pub const TRAY_MENU_LABEL: &str = "tray-menu"; -/// menu_id → (group, node_name) -static PROXY_NODE_MAP: OnceLock>> = OnceLock::new(); +// ===== 状态数据结构(发送给前端) ===== -fn proxy_node_map() -> &'static Mutex> { - PROXY_NODE_MAP.get_or_init(|| Mutex::new(Vec::new())) +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ProxyNodeInfo { + pub name: String, + pub delay: Option, + pub is_current: bool, +} + +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TrayMenuState { + pub proxy_running: bool, + pub monitor_running: bool, + pub proxy_group: Option, + pub proxy_nodes: Vec, + pub proxy_current: Option, } // ===== 伪节点过滤(与前端 ProxyModule 保持一致) ===== @@ -107,7 +114,6 @@ fn is_monitor_running(app: &AppHandle) -> bool { fn parse_main_group(proxies_response: &serde_json::Value) -> Option<(String, Vec, String)> { let proxies = proxies_response.get("proxies")?.as_object()?; - // 收集所有可选择组 let selector_groups: Vec<&str> = proxies .iter() .filter(|(_, v)| { @@ -120,7 +126,6 @@ fn parse_main_group(proxies_response: &serde_json::Value) -> Option<(String, Vec return None; } - // 优先选择名为 PROXY / Proxy / 节点选择 / 代理 的组 let preferred = selector_groups .iter() .find(|name| matches!(**name, "PROXY" | "Proxy" | "节点选择" | "代理")) @@ -156,9 +161,8 @@ fn parse_node_delay(proxies_response: &serde_json::Value, name: &str) -> Option< .map(|d| d as u32) } -// ===== 菜单构建 ===== +// ===== 获取代理节点数据(异步,调用 mihomo API) ===== -/// 获取代理节点数据(异步,调用 mihomo API) async fn fetch_proxy_nodes(app: &AppHandle) -> Option<(String, Vec<(String, Option)>, String)> { let mihomo = app.state::(); let proxies = mihomo.get_proxies().await.ok()?; @@ -170,224 +174,226 @@ async fn fetch_proxy_nodes(app: &AppHandle) -> Option<(String, Vec<(String, Opti Some((group, node_delays, now)) } -fn build_menu( - app: &AppHandle, - proxy_running: bool, - monitor_running: bool, - proxy_nodes: Option<(String, Vec<(String, Option)>, String)>, -) -> Result, tauri::Error> { - // ---------- 代理子菜单 ---------- - let enable_proxy = MenuItem::with_id( - app, - "proxy_enable", - "开启代理", - !proxy_running, - None::<&str>, - )?; - let disable_proxy = MenuItem::with_id( - app, - "proxy_disable", - "关闭代理", - proxy_running, - None::<&str>, - )?; - let proxy_sep = PredefinedMenuItem::separator(app)?; +// ===== 获取菜单状态 ===== - // 选择代理子菜单 - let (node_menu_items, node_sep) = build_proxy_select_items(app, proxy_running, &proxy_nodes)?; - - // "刷新节点列表"项(仅代理运行时创建,提前声明以保证引用生命周期) - let refresh = if proxy_running { - Some(MenuItem::with_id(app, "proxy_refresh", "🔄 刷新节点列表", true, None::<&str>)?) - } else { - None - }; - - // 组装选择代理子菜单 - let mut select_refs: Vec<&dyn IsMenuItem> = Vec::new(); - if !proxy_running { - // 只有一个 hint 项 - for item in &node_menu_items { - select_refs.push(item); - } - } else { - if let Some(ref r) = refresh { - select_refs.push(r); - } - if let Some(ref sep) = node_sep { - select_refs.push(sep); - } - for item in &node_menu_items { - select_refs.push(item); - } - } - let select_proxy = Submenu::with_items(app, "选择代理", proxy_running, &select_refs)?; - - // 组装代理子菜单 - let proxy_refs: Vec<&dyn IsMenuItem> = vec![&enable_proxy, &disable_proxy, &proxy_sep, &select_proxy]; - let proxy_submenu = Submenu::with_items(app, "代理", true, &proxy_refs)?; - - // ---------- 分隔线 ---------- - let sep1 = PredefinedMenuItem::separator(app)?; - - // ---------- OSD + Kernel ---------- - let osd_toggle = MenuItem::with_id(app, "osd_toggle", "切换 OSD", true, None::<&str>)?; - let kernel_restart = MenuItem::with_id( - app, - "kernel_restart", - "重启 Kernel", - monitor_running, - None::<&str>, - )?; - - // ---------- 分隔线 ---------- - let sep2 = PredefinedMenuItem::separator(app)?; - - // ---------- 新建下载 ---------- - let download_new = MenuItem::with_id(app, "download_new", "新建下载", true, None::<&str>)?; - - // ---------- 分隔线 ---------- - let sep3 = PredefinedMenuItem::separator(app)?; - - // ---------- 系统菜单 ---------- - let settings = MenuItem::with_id(app, "settings", "常规设置", true, None::<&str>)?; - let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?; - - // ---------- 组装主菜单 ---------- - let main_refs: Vec<&dyn IsMenuItem> = vec![ - &proxy_submenu, - &sep1, - &osd_toggle, - &kernel_restart, - &sep2, - &download_new, - &sep3, - &settings, - &quit, - ]; - Menu::with_items(app, &main_refs) -} - -/// 构建代理节点菜单项(返回 MenuItem 列表 + 可选分隔线) -fn build_proxy_select_items( - app: &AppHandle, - proxy_running: bool, - proxy_nodes: &Option<(String, Vec<(String, Option)>, String)>, -) -> Result<(Vec>, Option>), tauri::Error> { - if !proxy_running { - let hint = MenuItem::with_id(app, "proxy_noop", "代理未运行", false, None::<&str>)?; - return Ok((vec![hint], None)); - } - - let (group, nodes, now) = match proxy_nodes { - Some(data) => data, - None => { - let hint = MenuItem::with_id(app, "proxy_loading", "加载中…", false, None::<&str>)?; - return Ok((vec![hint], None)); - } - }; - - if nodes.is_empty() { - let hint = MenuItem::with_id(app, "proxy_noop", "无可用节点", false, None::<&str>)?; - return Ok((vec![hint], None)); - } - - let sep = PredefinedMenuItem::separator(app)?; - - // 清空并重建全局映射 - { - let mut map = proxy_node_map().lock().unwrap(); - map.clear(); - } - - let mut items: Vec> = Vec::new(); - - // 限制节点数量避免菜单过长 - for (idx, (name, delay)) in nodes.iter().take(30).enumerate() { - let delay_text = match delay { - Some(0) => "(超时)".to_string(), - Some(d) => format!("({}ms)", d), - None => "(未测试)".to_string(), - }; - let is_current = name == now; - let prefix = if is_current { "✓ " } else { " " }; - let text = format!("{}{} {}", prefix, name, delay_text); - - let menu_id = format!("proxy_node_{}", idx); - let item = MenuItem::with_id(app, &menu_id, &text, true, None::<&str>)?; - - // 记录映射 - { - let mut map = proxy_node_map().lock().unwrap(); - map.push((group.clone(), name.clone())); - } - - items.push(item); - } - - Ok((items, Some(sep))) -} - -// ===== 异步菜单重建 ===== - -async fn rebuild_menu(app: &AppHandle) { +pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState { let proxy_running = is_proxy_running(app); let monitor_running = is_monitor_running(app); - let proxy_nodes = if proxy_running { - fetch_proxy_nodes(app).await + + let (proxy_group, proxy_nodes, proxy_current) = if proxy_running { + match fetch_proxy_nodes(app).await { + Some((group, nodes, now)) => { + let node_infos: Vec = nodes + .iter() + .map(|(name, delay)| ProxyNodeInfo { + name: name.clone(), + delay: *delay, + is_current: name == &now, + }) + .collect(); + (Some(group), node_infos, if now.is_empty() { None } else { Some(now) }) + } + None => (None, vec![], None), + } } else { - None + (None, vec![], None) }; - if let Ok(menu) = build_menu(app, proxy_running, monitor_running, proxy_nodes) { - if let Some(tray) = app.tray_by_id(&TrayIconId::new("main")) { - let _ = tray.set_menu(Some(menu)); - } + TrayMenuState { + proxy_running, + monitor_running, + proxy_group, + proxy_nodes, + proxy_current, } } -// ===== 菜单事件处理 ===== +// ===== 显示/隐藏托盘菜单窗口 ===== -fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { - let id = event.id().as_ref().to_string(); - match id.as_str() { - "proxy_enable" => { - let app = app.clone(); - tauri::async_runtime::spawn(async move { - if let Err(e) = enable_proxy(&app).await { - eprintln!("[tray] 开启代理失败: {}", e); - send_notification("代理启动失败", &e); +/// 托盘菜单窗口尺寸(逻辑像素) +const MENU_W: f64 = 260.0; +const MENU_H: f64 = 420.0; + +/// 预创建隐藏的托盘菜单窗口(在应用启动时调用)。 +/// 窗口隐藏渲染,前端 onMounted 应用主题后保持隐藏状态,右键托盘时直接 show 即无延迟。 +pub fn precreate_tray_menu_window(app: &AppHandle) { + if app.get_webview_window(TRAY_MENU_LABEL).is_some() { + return; + } + + // 默认放在屏幕右下角(托盘常见位置附近),首次显示时会重新定位 + let (screen_w, screen_h) = get_work_area().unwrap_or((1920.0, 1080.0)); + let x = (screen_w as f64) - MENU_W - 20.0; + let y = (screen_h as f64) - MENU_H - 60.0; + + let win = match WebviewWindowBuilder::new( + app, + TRAY_MENU_LABEL, + WebviewUrl::App("index.html#tray-menu".into()), + ) + .title("Thing 菜单") + .inner_size(MENU_W, MENU_H) + .position(x, y) + .decorations(false) + .transparent(true) + .shadow(true) + .always_on_top(true) + .skip_taskbar(true) + .resizable(false) + .visible(false) + .focused(false) + .effects(EffectsBuilder::new().effects(vec![Effect::Mica]).build()) + .build() + { + Ok(w) => w, + Err(e) => { + eprintln!("[tray-menu] 预创建菜单窗口失败: {}", e); + return; + } + }; + + // 监听窗口失焦:自动隐藏(带 300ms 防抖,避免显示瞬间焦点未稳定而被立即隐藏) + let win_handle = win.clone(); + win.on_window_event(move |event| { + if let tauri::WindowEvent::Focused(false) = event { + let should_hide = { + let t = LAST_SHOW_TIME.lock().unwrap(); + match *t { + Some(time) => time.elapsed() > Duration::from_millis(300), + None => true, } - }); + }; + if should_hide { + let _ = win_handle.hide(); + } + } + }); + + eprintln!("[tray-menu] 菜单窗口已预创建(隐藏渲染)"); +} + +/// 在鼠标当前位置显示托盘菜单窗口。 +/// 窗口不存在则创建并显示,已存在则移动到鼠标位置并显示。 +pub fn show_tray_menu(app: &AppHandle) { + let (mx, my) = match get_cursor_pos() { + Some(p) => p, + None => return, + }; + + // GetCursorPos 在 Per-Monitor DPI V2 下返回物理像素,直接用作 PhysicalPosition + let win = app.get_webview_window(TRAY_MENU_LABEL); + let scale = win.as_ref() + .and_then(|w| w.scale_factor().ok()) + .unwrap_or(1.0); + + let w_phys = (MENU_W * scale) as i32; + let h_phys = (MENU_H * scale) as i32; + + // 托盘位于屏幕右下角,菜单始终出现在鼠标左上方 + let x = (mx - w_phys).max(0); + let y = (my - h_phys - 8).max(0); + + // 记录显示时间,用于失焦防抖 + { + let mut t = LAST_SHOW_TIME.lock().unwrap(); + *t = Some(Instant::now()); + } + + // 窗口已存在(预创建或之前显示过):移动 + 显示 + 发送状态 + if let Some(win) = win { + let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y })); + let _ = win.show(); + let _ = win.set_focus(); + let app_clone = app.clone(); + tauri::async_runtime::spawn(async move { + let state = get_tray_menu_state(&app_clone).await; + let _ = app_clone.emit("tray-menu-show", state); + }); + return; + } + + // 兜底:窗口未预创建(理论上不会走到,因为 create_tray_menu 已预创建) + precreate_tray_menu_window(app); + if let Some(win) = app.get_webview_window(TRAY_MENU_LABEL) { + let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y })); + let _ = win.show(); + let _ = win.set_focus(); + let app_clone = app.clone(); + tauri::async_runtime::spawn(async move { + let state = get_tray_menu_state(&app_clone).await; + let _ = app_clone.emit("tray-menu-show", state); + }); + } +} + +/// 预创建窗口完成后由前端调用(仅触发状态推送,不显示窗口——预创建模式下窗口保持隐藏)。 +pub fn show_window(app: &AppHandle) { + if app.get_webview_window(TRAY_MENU_LABEL).is_some() { + let app_clone = app.clone(); + tauri::async_runtime::spawn(async move { + let state = get_tray_menu_state(&app_clone).await; + let _ = app_clone.emit("tray-menu-show", state); + }); + } +} + +/// 隐藏托盘菜单窗口(不销毁,保留复用) +pub fn hide_tray_menu(app: &AppHandle) { + if let Some(win) = app.get_webview_window(TRAY_MENU_LABEL) { + let _ = win.hide(); + } +} + +/// 刷新菜单状态并发送给前端 +async fn refresh_and_emit_state(app: &AppHandle) { + let state = get_tray_menu_state(app).await; + let _ = app.emit("tray-menu-state-updated", state); +} + +// ===== Tauri 命令 ===== + +/// 执行菜单项动作(统一入口) +#[tauri::command] +pub async fn tray_menu_action( + action: String, + payload: Option, + app: AppHandle, +) -> Result<(), String> { + match action.as_str() { + "proxy_enable" => { + if let Err(e) = enable_proxy(&app).await { + eprintln!("[tray] 开启代理失败: {}", e); + send_notification(&app, "代理启动失败", &e); + } } "proxy_disable" => { - let app = app.clone(); - tauri::async_runtime::spawn(async move { - if let Err(e) = disable_proxy(&app).await { - eprintln!("[tray] 关闭代理失败: {}", e); - send_notification("代理关闭失败", &e); - } - }); + if let Err(e) = disable_proxy(&app).await { + eprintln!("[tray] 关闭代理失败: {}", e); + send_notification(&app, "代理关闭失败", &e); + } } "proxy_refresh" => { - let app = app.clone(); - tauri::async_runtime::spawn(async move { - // 先重建菜单(保留旧数据),然后并行测试延迟并再次重建 - rebuild_menu(&app).await; - test_all_delays_and_rebuild(&app).await; - }); + test_all_delays(&app).await; + } + "proxy_select_node" => { + if let Some(name) = payload + .as_ref() + .and_then(|p| p.get("name")) + .and_then(|n| n.as_str()) + { + if let Some(group) = get_proxy_group(&app).await { + select_proxy_node(&app, &group, name).await; + } + } } "osd_toggle" => { let _ = app.emit("tray:toggle-osd", ()); } "kernel_restart" => { - let app = app.clone(); - tauri::async_runtime::spawn(async move { - if let Err(e) = restart_kernel(&app).await { - eprintln!("[tray] 重启 Kernel 失败: {}", e); - send_notification("Kernel 重启失败", &e); - } - }); + if let Err(e) = restart_kernel(&app).await { + eprintln!("[tray] 重启 Kernel 失败: {}", e); + send_notification(&app, "Kernel 重启失败", &e); + } } "download_new" => { if let Some(window) = app.get_webview_window("main") { @@ -395,6 +401,7 @@ fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { window.set_focus().ok(); } let _ = app.emit("tray:new-download", ()); + hide_tray_menu(&app); } "settings" => { if let Some(window) = app.get_webview_window("main") { @@ -402,58 +409,43 @@ fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { window.set_focus().ok(); } let _ = app.emit("tray:open-settings", ()); + hide_tray_menu(&app); } "quit" => { - quit_cleanup(app); - } - id_str if id_str.starts_with("proxy_node_") => { - let idx: usize = id_str - .strip_prefix("proxy_node_") - .and_then(|s| s.parse().ok()) - .unwrap_or(usize::MAX); - let app = app.clone(); - tauri::async_runtime::spawn(async move { - if let Some((group, name)) = group_name_from_map(idx) { - select_proxy_node(&app, &group, &name).await; - } else { - eprintln!("[tray] 节点索引无效: {}", idx); - } - }); + hide_tray_menu(&app); + quit_cleanup(&app); } _ => {} } -} -/// 从全局映射中获取 (group, name) -fn group_name_from_map(idx: usize) -> Option<(String, String)> { - let map = proxy_node_map().lock().unwrap(); - map.get(idx).cloned() -} - -/// 选择代理节点并测试延迟 -async fn select_proxy_node(app: &AppHandle, group: &str, name: &str) { - let mihomo = app.state::(); - match mihomo.select_proxy(group, name).await { - Ok(()) => { - // 测试延迟 - let delay = mihomo - .test_delay(name, "https://www.gstatic.com/generate_204", 5000) - .await - .ok(); - let delay_text = match delay { - Some(0) => "超时".to_string(), - Some(d) => format!("{}ms", d), - None => "未测速".to_string(), - }; - send_notification("节点已切换", &format!("{}\n延迟: {}", name, delay_text)); - // 重建菜单 - rebuild_menu(app).await; - } - Err(e) => { - eprintln!("[tray] 切换节点失败: {}", e); - send_notification("切换节点失败", &e); - } + // 刷新状态并发送给前端(quit 除外,quit 后进程已退出) + if action != "quit" { + refresh_and_emit_state(&app).await; } + + Ok(()) +} + +/// 隐藏托盘菜单窗口(前端调用) +#[tauri::command] +pub async fn tray_menu_hide(app: AppHandle) -> Result<(), String> { + hide_tray_menu(&app); + Ok(()) +} + +/// 显示已创建的菜单窗口(前端 onMounted 后调用) +#[tauri::command] +pub async fn tray_menu_show_window(app: AppHandle) -> Result<(), String> { + show_window(&app); + Ok(()) +} + +/// 获取当前代理组名 +async fn get_proxy_group(app: &AppHandle) -> Option { + let mihomo = app.state::(); + let proxies = mihomo.get_proxies().await.ok()?; + let (group, _, _) = parse_main_group(&proxies)?; + Some(group) } // ===== 代理开启/关闭 ===== @@ -485,24 +477,22 @@ async fn enable_proxy(app: &AppHandle) -> Result<(), String> { match &best { Some((name, delay)) => { send_notification( + app, "代理已开启", &format!("当前节点: {} ({}ms)", name, delay), ); } None => { - send_notification("代理已开启", "所有节点均超时,未自动选择"); + send_notification(app, "代理已开启", "所有节点均超时,未自动选择"); } } } else { - send_notification("代理已开启", "无可用节点"); + send_notification(app, "代理已开启", "无可用节点"); } // 5. 开启系统代理 mihomo.enable_system_proxy()?; - // 6. 重建菜单 - rebuild_menu(app).await; - Ok(()) } @@ -516,10 +506,7 @@ async fn disable_proxy(app: &AppHandle) -> Result<(), String> { // 停止 mihomo pm.stop("proxy")?; - send_notification("代理已关闭", "mihomo 已停止,系统代理已清除"); - - // 重建菜单 - rebuild_menu(app).await; + send_notification(app, "代理已关闭", "mihomo 已停止,系统代理已清除"); Ok(()) } @@ -547,14 +534,12 @@ async fn test_and_select_best( let results = join_all(futures).await; - // 找出有效延迟中最低的 let best = results .into_iter() .filter_map(|(name, delay)| delay.map(|d| (name, d))) .filter(|(_, d)| *d > 0) .min_by_key(|(_, d)| *d); - // 选择最优节点 if let Some((ref name, _)) = best { let _ = mihomo.select_proxy(group, name).await; } @@ -562,21 +547,18 @@ async fn test_and_select_best( best } -/// 测试所有节点延迟后重建菜单 -async fn test_all_delays_and_rebuild(app: &AppHandle) { +/// 并行测试所有节点延迟(更新 mihomo 内部 history) +async fn test_all_delays(app: &AppHandle) { let mihomo = app.state::(); let proxies = match mihomo.get_proxies().await { Ok(p) => p, - Err(_) => { - return; - } + Err(_) => return, }; - let (_group, nodes, _) = match parse_main_group(&proxies) { + let (_group, nodes, _now) = match parse_main_group(&proxies) { Some(g) => g, None => return, }; - // 并行测试延迟(更新 mihomo 内部 history) use futures_util::future::join_all; let futures: Vec<_> = nodes .iter() @@ -587,9 +569,30 @@ async fn test_all_delays_and_rebuild(app: &AppHandle) { }) .collect(); join_all(futures).await; +} - // 再次获取 proxies(此时 history 已更新)并重建菜单 - rebuild_menu(app).await; +// ===== 选择代理节点并测试延迟 ===== + +async fn select_proxy_node(app: &AppHandle, group: &str, name: &str) { + let mihomo = app.state::(); + match mihomo.select_proxy(group, name).await { + Ok(()) => { + let delay = mihomo + .test_delay(name, "https://www.gstatic.com/generate_204", 5000) + .await + .ok(); + let delay_text = match delay { + Some(0) => "超时".to_string(), + Some(d) => format!("{}ms", d), + None => "未测速".to_string(), + }; + send_notification(app, "节点已切换", &format!("{}\n延迟: {}", name, delay_text)); + } + Err(e) => { + eprintln!("[tray] 切换节点失败: {}", e); + send_notification(app, "切换节点失败", &e); + } + } } // ===== 等待 mihomo API 就绪 ===== @@ -621,103 +624,25 @@ async fn restart_kernel(app: &AppHandle) -> Result<(), String> { // 重新启动 monitor.start_with_subscription(app).await?; - send_notification("Kernel 已重启", "ThingHK 监控内核已重新启动"); - rebuild_menu(app).await; + send_notification(app, "Kernel 已重启", "ThingHK 监控内核已重新启动"); Ok(()) } // ===== Windows 通知 ===== -#[cfg(windows)] -fn send_notification(title: &str, message: &str) { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - - // 转义 PowerShell 单引号 - let title_escaped = title.replace('\'', "''"); - let msg_escaped = message.replace('\'', "''").replace('\n', "`n"); - - let script = format!( - "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')|Out-Null;\ - $n=New-Object System.Windows.Forms.NotifyIcon;\ - $n.Icon=[System.Drawing.SystemIcons]::Information;\ - $n.Visible=$true;\ - $n.BalloonTipTitle='{}';\ - $n.BalloonTipText='{}';\ - $n.ShowBalloonTip(5000);\ - Start-Sleep -Seconds 6;\ - $n.Dispose()", - title_escaped, msg_escaped - ); - - let _ = std::process::Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-WindowStyle", - "Hidden", - "-Command", - &script, - ]) - .creation_flags(CREATE_NO_WINDOW) - .spawn(); +fn send_notification(app: &AppHandle, title: &str, message: &str) { + use tauri_plugin_notification::NotificationExt; + let _ = app + .notification() + .builder() + .title(title) + .body(message) + .show(); } -#[cfg(not(windows))] -fn send_notification(_title: &str, _message: &str) { - // 非 Windows 平台不显示通知 -} +// ===== 退出清理 ===== -// ===== 创建托盘 ===== - -/// 创建自定义托盘菜单(在 setup 中调用) -pub fn create_tray_menu(app: &AppHandle) -> Result<(), tauri::Error> { - let proxy_running = is_proxy_running(app); - let monitor_running = is_monitor_running(app); - - // 初始菜单(不含代理节点,异步获取后替换) - let menu = build_menu(app, proxy_running, monitor_running, None)?; - - let _tray = TrayIconBuilder::with_id("main") - .icon(app.default_window_icon().unwrap().clone()) - .tooltip("Thing") - .menu(&menu) - .show_menu_on_left_click(false) - .on_menu_event(handle_menu_event) - .on_tray_icon_event(|tray, event| { - let app = tray.app_handle().clone(); - match event { - TrayIconEvent::Click { - button: MouseButton::Left, - .. - } => { - if let Some(window) = app.get_webview_window("main") { - window.show().ok(); - window.set_focus().ok(); - } - } - TrayIconEvent::Enter { .. } => { - // 鼠标进入托盘区域时预刷新菜单 - tauri::async_runtime::spawn(async move { - rebuild_menu(&app).await; - }); - } - _ => {} - } - }) - .build(app)?; - - // 异步获取代理节点并重建菜单 - let app_clone = app.clone(); - tauri::async_runtime::spawn(async move { - rebuild_menu(&app_clone).await; - }); - - Ok(()) -} - -/// 退出应用时调用(与 quit_app 命令共享清理逻辑) pub fn quit_cleanup(app: &AppHandle) { if let Some(mihomo) = app.try_state::() { mihomo.cleanup_on_exit(); @@ -725,8 +650,18 @@ pub fn quit_cleanup(app: &AppHandle) { if let Some(engine) = app.try_state::() { engine.cleanup_on_exit(); } + // monitor.cleanup_on_exit 是 async,在 tokio worker 线程中直接 block_on 会 panic, + // 放到独立 OS 线程执行 block_on,避免嵌套 runtime。 if let Some(monitor) = app.try_state::() { - tauri::async_runtime::block_on(monitor.cleanup_on_exit(app)); + let app_clone = app.clone(); + let monitor_clone = monitor.inner().clone(); + std::thread::spawn(move || { + tauri::async_runtime::block_on(async move { + monitor_clone.cleanup_on_exit(&app_clone).await; + }); + }) + .join() + .ok(); } if let Some(clip) = app.try_state::() { clip.stop(); @@ -736,3 +671,42 @@ pub fn quit_cleanup(app: &AppHandle) { } app.exit(0); } + +// ===== 创建托盘 ===== + +/// 创建自定义托盘(在 setup 中调用) +/// 不使用原生菜单,改用 WebviewWindow 弹窗实现自定义风格。 +pub fn create_tray_menu(app: &AppHandle) -> Result<(), tauri::Error> { + let _tray = TrayIconBuilder::with_id("main") + .icon(app.default_window_icon().unwrap().clone()) + .tooltip("Thing") + .on_tray_icon_event(|tray, event| { + let app = tray.app_handle().clone(); + match event { + TrayIconEvent::Click { + button: MouseButton::Left, + .. + } => { + // 左键:显示主窗口 + if let Some(window) = app.get_webview_window("main") { + window.show().ok(); + window.set_focus().ok(); + } + } + TrayIconEvent::Click { + button: MouseButton::Right, + .. + } => { + // 右键:显示自定义菜单窗口 + show_tray_menu(&app); + } + _ => {} + } + }) + .build(app)?; + + // 预创建隐藏的菜单窗口,让前端提前渲染主题和内容,右键时直接 show 无延迟 + precreate_tray_menu_window(app); + + Ok(()) +} diff --git a/src/main.ts b/src/main.ts index ba8f7f7..a45ccab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ window.addEventListener('unhandledrejection', (event) => { }) // ===== OSD 窗口模式检测 ===== -// 通过 URL hash 识别:#osd-overlay(悬浮窗)/ #clipboard-popup(剪贴板快捷弹窗) +// 通过 URL hash 识别:#osd-overlay(悬浮窗)/ #clipboard-popup(剪贴板快捷弹窗)/ #tray-menu(托盘菜单) // 这些窗口是精简的独立 Vue 应用,不加载主应用的 store 和模块 const winHash = window.location.hash if (winHash === '#osd-overlay') { @@ -31,6 +31,12 @@ if (winHash === '#osd-overlay') { const app = createApp(ClipboardPopup) app.mount('#app') }) +} else if (winHash === '#tray-menu') { + logger.info(`托盘菜单窗口启动: ${winHash}`) + void import('./modules/tray/TrayMenu.vue').then(({ default: TrayMenu }) => { + const app = createApp(TrayMenu) + app.mount('#app') + }) } else { // ===== 主应用模式 ===== void import('./App.vue').then(async ({ default: App }) => { diff --git a/src/modules/monitor/MonitorModule.vue b/src/modules/monitor/MonitorModule.vue index 57e1082..161fa06 100644 --- a/src/modules/monitor/MonitorModule.vue +++ b/src/modules/monitor/MonitorModule.vue @@ -44,20 +44,6 @@ const tabsListRef = useModuleTabs(activeTab, [ { value: 'settings', label: '设置' }, ]) -// ===== 关键指标历史 buffer(用于概览页 sparkline,不引入外部图表库) ===== -// 容量 30 点,约对应 30 秒(fast 通道 1s 推送一次) -const HISTORY_LEN = 30 -const cpuTempHistory = ref([]) -const gpuTempHistory = ref([]) -const netDownHistory = ref([]) -const netUpHistory = ref([]) - -function pushHistory(buf: typeof cpuTempHistory, v: number | null) { - if (v == null || !isFinite(v)) return - buf.value.push(v) - if (buf.value.length > HISTORY_LEN) buf.value.shift() -} - // ===== 关键指标 computed ===== /** GPU 分组 id(兼容 intel/amd/nvidia) */ @@ -69,9 +55,34 @@ const gpuGroupId = computed(() => { }) const cpuModel = computed(() => store.groupById['cpu']?.sensors[0]?.hardwareName ?? null) -const cpuTemp = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'temperature' })) + +/** 取首个 >0 的传感器值:用于过滤 AMD Ryzen 上 LHB 返回 0 的无效读数(温度/功耗/时钟为 0 不可信) */ +function findPositiveValue(groupId: string, matchers: Array<{ name?: string; hardwareName?: string; type?: string }>): number | null { + for (const m of matchers) { + const v = store.findSensorValue(groupId, m) + if (v != null && v > 0 && isFinite(v)) return v + } + return null +} + +// CPU 温度:Intel 命名 "CPU Package",AMD Ryzen 命名 "Core (Tctl/Tdie)"; +// LHB 0.9.5 对 Ryzen 9000 系列支持不全,可能返回 0,用 >0 守卫过滤。 +const cpuTemp = computed(() => + findPositiveValue('cpu', [ + { name: 'CPU Package', type: 'temperature' }, // Intel + { name: 'Core (Tctl/Tdie)', type: 'temperature' }, // AMD Ryzen + { type: 'temperature' }, // 兜底:任意温度 + ]) +) const cpuLoad = computed(() => store.findSensorValue('cpu', { name: 'CPU Total', type: 'load' })) -const cpuPower = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'power' })) +// CPU 功耗:Intel "CPU Package",AMD Ryzen "Package" +const cpuPower = computed(() => + findPositiveValue('cpu', [ + { name: 'CPU Package', type: 'power' }, // Intel + { name: 'Package', type: 'power' }, // AMD Ryzen + { type: 'power' }, // 兜底:任意功耗 + ]) +) const gpuModel = computed(() => { const gid = gpuGroupId.value @@ -121,7 +132,10 @@ const memModuleModels = computed(() => { return Array.from(models) }) -/** 存储硬盘列表(按 hardwareName 分组,提取温度/使用率/容量) */ +/** 存储硬盘列表(按 hardwareName 分组,提取温度/使用率/容量) + * LHB 0.9.5 已知 bug:MBR + XINT13 Extended 分区布局下,Free Space 可能返回 + * ≈ 2^64/1e9 GB(无符号回绕),Used Space load 也可能为巨大负值。 + * 这里对 LHB 原始值做有效性校验,异常时降级为 null(UI 显示 "--")。 */ interface StorageDrive { name: string temp: number | null @@ -139,28 +153,34 @@ const storageDrives = computed(() => { } return Array.from(byHw.entries()).map(([hw, sensors]) => { const temp = sensors.find(s => s.type === 'temperature')?.value ?? null - const usedPct = sensors.find(s => s.name === 'Used Space')?.value ?? null - const totalGB = sensors.find(s => s.name === 'Total Space')?.value ?? null - const freeGB = sensors.find(s => s.name === 'Free Space')?.value ?? null - const usedGB = totalGB != null && freeGB != null ? totalGB - freeGB : null + // Used Space 是 load 百分比,理论上 [0,100];LHB 在 MBR+Extended 布局下可能返回 -922153000% 等 + const rawUsedPct = sensors.find(s => s.name === 'Used Space' && s.type === 'load')?.value ?? null + const usedPct = rawUsedPct != null && isFinite(rawUsedPct) && rawUsedPct >= 0 && rawUsedPct <= 100 + ? rawUsedPct + : null + // Total Space 在 LHB 中通常正确(即便 Free Space 异常) + const rawTotal = sensors.find(s => s.name === 'Total Space' && s.type === 'data')?.value ?? null + const totalGB = rawTotal != null && isFinite(rawTotal) && rawTotal > 0 && rawTotal < 1e6 + ? rawTotal + : null + // Free Space 在 MBR+Extended 布局下可能返回 ≈ 1.8446744e10 GB(UINT64_MAX / 1e9) + const rawFree = sensors.find(s => s.name === 'Free Space' && s.type === 'data')?.value ?? null + const freeGB = rawFree != null && isFinite(rawFree) && rawFree >= 0 && totalGB != null && rawFree <= totalGB + ? rawFree + : null + // 优先用 total - free 计算 usedGB;free 异常时回退用 usedPct * total / 100 + let usedGB: number | null = null + if (totalGB != null) { + if (freeGB != null) { + usedGB = totalGB - freeGB + } else if (usedPct != null) { + usedGB = totalGB * usedPct / 100 + } + } return { name: hw, temp, usedPct, totalGB, usedGB } }) }) -// 监听快照变化,提取温度历史(用于 CPU/GPU 温度 sparkline) -watch(() => store.snapshot, () => { - pushHistory(cpuTempHistory, cpuTemp.value) - pushHistory(gpuTempHistory, gpuTemp.value) -}, { deep: false }) - -// 监听网速事件,记录历史(用于网速 sparkline) -watch(() => store.networkSpeed, (v) => { - if (v) { - pushHistory(netDownHistory, v.downloadBps) - pushHistory(netUpHistory, v.uploadBps) - } -}, { deep: false }) - // 网速格式化(computed 避免模板中重复调用) const downSpeed = computed(() => fmtSpeed(store.networkSpeed?.downloadBps ?? null)) const upSpeed = computed(() => fmtSpeed(store.networkSpeed?.uploadBps ?? null)) @@ -258,22 +278,6 @@ const stateMeta: Record = { error: { text: '错误', class: 'bg-red-500/15 text-red-600 dark:text-red-400' }, } -// ===== sparkline 路径生成(无外部依赖) ===== -function sparklinePath(data: number[], opts: { min?: number; max?: number; w?: number; h?: number } = {}): string { - if (data.length < 2) return '' - const w = opts.w ?? 100 - const h = opts.h ?? 28 - const min = opts.min ?? Math.min(...data) - const max = opts.max ?? Math.max(...data) - const range = max - min || 1 - const step = w / (data.length - 1) - return data.map((v, i) => { - const x = i * step - const y = h - ((v - min) / range) * h - return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}` - }).join(' ') -} - // ===== 分组列表(详细页用) ===== const groups = computed(() => store.snapshot?.groups ?? []) @@ -651,15 +655,20 @@ const osdConfig = ref(loadOsdConfig()) /** 传感器名称中英文字典(覆盖常见 LHB 传感器名 + 硬件名) */ const SENSOR_NAME_ZH: Record = { - // CPU - 'CPU Package': 'CPU 封装', - 'CPU Total': 'CPU 总负载', - 'CPU Core Average': 'CPU 平均', - 'CPU Graphics': 'CPU 核显', - 'CPU DRAM': 'CPU 内存', - 'CPU Cores': 'CPU 核心', - 'CPU Bus': 'CPU 总线', - 'CPU Core': 'CPU 核心', + // CPU 通用(Intel 命名) + 'CPU Total': '总使用率', + 'CPU Core Average': '平均温度', + 'CPU Core Max': '最大使用率', + 'CPU Graphics': '核显', + 'CPU DRAM': '内存功耗', + 'CPU Cores': '核心功耗', + 'CPU Bus': '总线功耗', + 'Bus Speed': '总线频率', + 'Distance to TjMax': 'TjMax 距离', + // AMD Ryzen + 'Core (Tctl/Tdie)': '封装温度', + 'Cores (Average)': '平均频率', + 'Cores (Average Effective)': '平均有效频率', // GPU 'GPU Core': 'GPU 核心', 'GPU Memory': 'GPU 显存', @@ -672,11 +681,13 @@ const SENSOR_NAME_ZH: Record = { 'GPU Memory Total': '总显存', 'GPU Memory Used': '已用显存', 'D3D 3D': '3D 引擎', + 'D3D Copy': '拷贝引擎', + 'D3D VideoDecode': '视频解码', + 'D3D VideoProcessing': '视频处理', // 内存 - 'Memory': '内存', 'Memory Used': '已用', 'Memory Available': '可用', - 'Virtual Memory': '虚拟内存', + 'Virtual Memory': '虚拟内存使用率', 'Virtual Memory Used': '虚拟已用', 'Virtual Memory Available': '虚拟可用', // 存储 @@ -692,7 +703,7 @@ const SENSOR_NAME_ZH: Record = { 'System Fan': '系统风扇', 'Motherboard': '主板', 'Motherboard Temperature': '主板温度', - 'CPU Socket': 'CPU 插槽', + 'CPU Socket': 'CPU 插槽温度', // 电池 'Battery Level': '电量', 'Battery Charge': '充电功率', @@ -702,6 +713,23 @@ const SENSOR_NAME_ZH: Record = { 'Battery Wear Level': '电池损耗', } +/** 同名传感器按 type 消歧的字典(key 格式:sensorName|type)。 + * 用于解决 Intel/AMD 同一传感器名在不同 type 下含义不同的问题。 + * 例如 'CPU Package' 在 temperature 是封装温度,在 power 是封装功耗; + * 'Memory' 在 load 是使用率,在 data 是内存容量。 */ +const SENSOR_NAME_ZH_TYPED: Record = { + 'CPU Package|temperature': '封装温度', + 'CPU Package|power': '封装功耗', + 'Package|power': '封装功耗', + 'Package|temperature': '封装温度', + 'Memory|load': '内存使用率', + 'Memory|data': '内存', + 'Memory|smalldata': '内存', + 'Cores (Average)|clock': '平均频率', + 'Cores (Average)|power': '核心平均功耗', + 'Cores (Average Effective)|clock': '平均有效频率', +} + /** 硬件名中英文(用于分组标题等) */ const HW_NAME_ZH: Record = { 'Total Memory': '总内存', @@ -775,7 +803,8 @@ function fullColloquialLabel(item: OsdItem): string { return hw || type || (SENSOR_NAME_ZH[item.sensorName] ?? item.sensorName) } -/** 传感器显示名翻译:根据 labelLanguage 返回中文或英文 */ +/** 传感器显示名翻译:根据 labelLanguage 返回中文或英文 + * 中文优先智能"按词对应"翻译,找不到时回退到通俗组合 */ function sensorLabel(item: OsdItem): string { // 特殊项 if (item.special) { @@ -784,7 +813,10 @@ function sensorLabel(item: OsdItem): string { return osdConfig.value.labelLanguage === 'zh' ? meta.zh : meta.en } if (osdConfig.value.labelLanguage === 'en') return item.sensorName - // 使用通俗描述:CPU温度、CPU功耗、CPU使用率 等 + // 优先:智能精准翻译(与 Dialog 保持一致) + const smart = smartSensorLabelZh(item.sensorName, item.type) + if (smart) return smart + // 回退:通俗描述 return fullColloquialLabel(item) } @@ -795,8 +827,7 @@ function hwLabel(item: OsdItem): string { } /** AvailableSensor 的传感器名翻译(用于选择 Dialog) - * 使用更明显的名字:负载→使用率,温度/功率→XXX温度/XXX功率 - * 容量类(data/smalldata)用字典翻译区分已用/可用,避免重名 */ + * 优先智能"按词对应"翻译(精准且唯一),找不到时回退到通俗组合。 */ function sensorLabelAvail(s: AvailableSensor): string { if (s.special) { const meta = SPECIAL_SENSOR_META[s.special] @@ -804,26 +835,28 @@ function sensorLabelAvail(s: AvailableSensor): string { return osdConfig.value.labelLanguage === 'zh' ? meta.zh : meta.en } if (osdConfig.value.labelLanguage === 'en') return s.sensorName + + // 优先:智能精准翻译(typed 字典 + 字典 + 模式匹配) + const smart = smartSensorLabelZh(s.sensorName, s.type) + if (smart) return smart + // 容量类(data/smalldata):用字典翻译 + 硬件名区分(已用内存/可用内存/已用空间/可用空间) if (s.type === 'data' || s.type === 'smalldata') { const dictName = SENSOR_NAME_ZH[s.sensorName] if (dictName) { const hw = shortHardwareLabelAvail(s) - // 存储类用硬盘型号 const prefix = s.groupId === 'storage' ? truncateHwName(s.hardwareName) : hw return prefix ? `${prefix}${dictName}` : dictName } return SENSOR_NAME_ZH[s.sensorName] ?? s.sensorName } - // 中文:用 通俗类型后缀 生成更明显的名字(如 CPU温度、CPU使用率、GPU功耗) + // 回退:通俗类型组合(短前缀 + 类型后缀) const type = colloquialTypeLabel({ type: s.type } as OsdItem) const hw = shortHardwareLabelAvail(s) - // 存储类用硬件名(硬盘型号)+ 类型 if (s.groupId === 'storage') { return `${truncateHwName(s.hardwareName)}${type}` } if (hw && type) return `${hw}${type}` - // 回退到字典翻译 return SENSOR_NAME_ZH[s.sensorName] ?? s.sensorName } @@ -851,6 +884,76 @@ function hwLabelAvail(s: AvailableSensor): string { return HW_NAME_ZH[s.hardwareName] ?? s.hardwareName } +/** 类型后缀(中文,用于组合"核心 N 频率"、"封装温度"等精准翻译) */ +function typeSuffixZh(type: string): string { + switch (type) { + case 'temperature': return '温度' + case 'load': return '使用率' + case 'power': return '功耗' + case 'clock': return '频率' + case 'voltage': return '电压' + case 'fan': return '风扇' + case 'throughput': return '速率' + case 'level': return '等级' + case 'frequency': return '频率' + case 'control': return '控制' + case 'factor': return '因子' + case 'timespan': return '时长' + case 'energy': return '能量' + default: return '' + } +} + +/** 智能"按词对应"翻译:根据 sensorName + type 生成精准中文标签 + * 优先级:① typed 字典(同名不同义,如 CPU Package temperature vs power) + * ② 普通字典(固定短语) + * ③ 模式匹配(带核心序号、CCD、D3D 引擎等) + * ④ 返回 null 交由调用方回退 + * 设计目标:每个传感器产出唯一中文标签,避免"CPU功耗 ×8"这种重复 */ +function smartSensorLabelZh(name: string, type: string): string | null { + // ① typed 字典:同名 + type 消歧 + const typedKey = `${name}|${type}` + if (SENSOR_NAME_ZH_TYPED[typedKey]) return SENSOR_NAME_ZH_TYPED[typedKey] + + // ② 普通字典 + if (SENSOR_NAME_ZH[name]) return SENSOR_NAME_ZH[name] + + // ③ 模式匹配 + let m: RegExpMatchArray | null + // CPU Core #N / Core #N → 核心 N + 类型后缀(load→使用率, clock→频率, power→功耗...) + if ((m = name.match(/^(?:CPU )?Core #(\d+)$/))) { + return `核心 ${m[1]}${typeSuffixZh(type)}` + } + // CPU Core #N (Effective) / Core #N (Effective) → 核心 N 有效频率 + if ((m = name.match(/^(?:CPU )?Core #(\d+) \(Effective\)$/))) { + return `核心 ${m[1]} 有效${typeSuffixZh(type)}` + } + // Core #N (SMU) → 核心 N (SMU) 功耗 + if ((m = name.match(/^Core #(\d+) \(SMU\)$/))) { + return `核心 ${m[1]} (SMU)${typeSuffixZh(type)}` + } + // CCD1 (Tdie) → CCD1 (Tdie) 温度 + if ((m = name.match(/^CCD(\d+) \(Tdie\)$/))) { + return `CCD${m[1]} (Tdie) 温度` + } + // CCD#N (Tccd#N) → CCD#N (Tccd#N) 温度 + if ((m = name.match(/^CCD(\d+) \(Tccd\d+\)$/))) { + return `CCD${m[1]} 温度` + } + // D3D 引擎族:D3D 3D / D3D Copy / D3D VideoDecode / D3D VideoProcessing + if ((m = name.match(/^D3D\s+(.+)$/))) { + const d3dMap: Record = { + '3D': '3D 引擎', + 'Copy': '拷贝引擎', + 'VideoDecode': '视频解码', + 'VideoProcessing': '视频处理', + } + if (d3dMap[m[1]]) return d3dMap[m[1]] + } + + return null +} + /** * 判断传感器是否为"常用项"。 * 规则:基于 name + type + hardwareName 模式匹配,挑选日常监控最关注的指标。 @@ -864,14 +967,14 @@ function isCommon(s: { sensorName: string; hardwareName: string; type: string; s const hw = s.hardwareName.toLowerCase() const nameLc = name.toLowerCase() - // CPU 常用:Package 温度 / Total 负载 / Package 功耗 / Graphics 核显 / Core Average 温度 + // CPU 常用:封装温度 / Total 负载 / 封装功耗 / Graphics 核显 / Core Average 温度 + // 兼容 Intel ("CPU Package") 与 AMD Ryzen ("Core (Tctl/Tdie)" / "Package") 命名 // 其余(分核负载、分核温度、时钟、总线、各路功耗)归详细 - if (hw.includes('cpu') || nameLc.startsWith('cpu')) { - if (name === 'CPU Package' && type === 'temperature') return true + if (hw.includes('cpu') || nameLc.startsWith('cpu') || hw.includes('amd ryzen')) { + if (type === 'temperature' && (name === 'CPU Package' || name === 'Core (Tctl/Tdie)' || name === 'CPU Core Average')) return true if (name === 'CPU Total' && type === 'load') return true - if (name === 'CPU Package' && type === 'power') return true + if (type === 'power' && (name === 'CPU Package' || name === 'Package')) return true if (name === 'CPU Graphics' && (type === 'load' || type === 'temperature')) return true - if (name === 'CPU Core Average' && type === 'temperature') return true return false } @@ -951,6 +1054,8 @@ const availableSensors = computed(() => { special: 'net-down', }) for (const g of store.snapshot?.groups ?? []) { + // 悬浮窗不显示存储分组(硬盘容量/温度等已在主界面监控,OSD 场景无需) + if (g.id === 'storage') continue const groupName = groupMeta[g.id]?.name ?? g.name for (const s of g.sensors) { const key = `${g.id}/${s.hardwareName}/${s.name}/${s.type}`.replace(/\s+/g, '_').toLowerCase() @@ -1019,7 +1124,11 @@ const osdPickSelected = ref>({}) function openOsdPickDialog() { const items = osdConfig.value.overlayItems const selected: Record = {} - for (const it of items) selected[it.key] = true + // 存储分组已从 OSD 选择中移除,已选的存储项不预选(确认时自动清理) + for (const it of items) { + if (it.groupId === 'storage') continue + selected[it.key] = true + } osdPickSelected.value = selected osdPickDialogOpen.value = true } @@ -1028,8 +1137,9 @@ function confirmOsdPick() { // 收集所有勾选项(保留原有项对象,新增项从 availableSensors 构造) const oldItems = osdConfig.value.overlayItems const newItems: OsdItem[] = [] - // 保留原有项 + // 保留原有项(存储项不再支持,直接丢弃) for (const old of oldItems) { + if (old.groupId === 'storage') continue if (osdPickSelected.value[old.key]) { newItems.push(old) } @@ -1048,13 +1158,12 @@ function confirmOsdPick() { }) } } - // 按默认顺序排序:CPU → GPU → 内存 → 网络 → 存储 → 其余 + // 按默认顺序排序:CPU → GPU → 内存 → 网络 → 其余 const groupOrder = (id: string): number => { if (id === 'cpu') return 0 if (id.startsWith('gpu')) return 1 if (id === 'memory') return 2 if (id === 'network') return 3 - if (id === 'storage') return 4 return 9 } newItems.sort((a, b) => groupOrder(a.groupId) - groupOrder(b.groupId)) @@ -1138,53 +1247,6 @@ function onBgAlphaInput(alpha: number) { updateOsdConfig('bgColor', `rgba(${r}, ${g}, ${b}, ${a})`) } -/** 根据 OSD item 查找当前快照中的传感器值(支持特殊项) */ -function getOsdItemValue(item: OsdItem): number | null { - // 特殊项:网速 - if (item.special === 'net-up') return store.networkSpeed?.uploadBps ?? null - if (item.special === 'net-down') return store.networkSpeed?.downloadBps ?? null - const g = store.groupById[item.groupId] - if (!g) return null - const s = g.sensors.find(s => - s.hardwareName === item.hardwareName && s.name === item.sensorName - ) - return s?.value ?? null -} - -/** 格式化 OSD 显示值(简洁模式,不含单位后缀;网速已含单位字符串) */ -function fmtOsdValue(v: number | null, type: string, _unit: string, special?: string): string { - if (v == null || !isFinite(v)) return '--' - // 网速特殊项:自适应 KB/s 或 MB/s(值已含单位字符串) - if (special === 'net-up' || special === 'net-down') { - if (v >= 1_048_576) return (v / 1_048_576).toFixed(2) + ' MB/s' - if (v >= 1024) return (v / 1024).toFixed(1) + ' KB/s' - return v.toFixed(0) + ' B/s' - } - const digits = (type === 'voltage' || type === 'power') ? 2 - : (type === 'temperature' || type === 'load' || type === 'level') ? 0 - : 1 - return v.toFixed(digits) -} - -/** 获取单位后缀(简洁模式,网速已含单位返回空) */ -function unitSuffix(item: OsdItem): string { - if (item.special === 'net-up' || item.special === 'net-down') return '' - if (!osdConfig.value.showUnit) return '' - switch (item.type) { - case 'temperature': return '°C' - case 'load': return '%' - case 'power': return 'W' - case 'voltage': return 'V' - case 'fan': return 'RPM' - case 'clock': - case 'frequency': return 'MHz' - case 'data': - case 'smalldata': return 'GB' - case 'level': return '%' - default: return item.unit || '' - } -} - /** 将 hex 颜色 + 不透明度(0-100) 转为 rgba 字符串 */ function withOpacity(hex: string, opacityPct: number): string { const a = Math.max(0, Math.min(100, opacityPct)) / 100 @@ -1210,42 +1272,6 @@ function osdItemColor(item: OsdItem): string { return withOpacity(osdConfig.value.fontColor, opacity) } -/** 预览用分组(group / multiline 布局) */ -const osdPreviewGroups = computed(() => { - const items = osdConfig.value.overlayItems - if (!items?.length) return [] - const groups: { key: string; label: string; items: OsdItem[] }[] = [] - const map = new Map() - const isEn = osdConfig.value.labelLanguage === 'en' - for (const item of items) { - let gkey: string - if (item.special === 'net-up' || item.special === 'net-down') gkey = 'network' - else if (item.groupId.startsWith('gpu')) gkey = 'gpu' - else gkey = item.groupId - let g = map.get(gkey) - if (!g) { - const label = isEn - ? ({ cpu: 'CPU', gpu: 'GPU', memory: 'RAM', storage: 'DISK', network: 'NET' }[gkey] ?? gkey.toUpperCase().slice(0, 6)) - : ({ cpu: 'CPU', gpu: 'GPU', memory: '内存', storage: '存储', network: '网络' }[gkey] ?? gkey) - g = { key: gkey, label, items: [] } - map.set(gkey, g) - groups.push(g) - } - g.items.push(item) - } - return groups -}) - -/** 预览用布局标签 */ -const osdLayoutLabel = computed(() => { - switch (osdConfig.value.layout) { - case 'single': return '单行' - case 'group': return '分组横排' - case 'multiline': return '多行' - default: return '' - } -}) - // ===== OSD 窗口管理(实际创建/隐藏 Tauri 窗口并推送数据) ===== const OSD_OVERLAY_LABEL = 'osd-overlay' /** 抑制百分比 watch 的程序定位标志:拖动 onMoved 更新百分比时置 true,避免触发 resetOverlayPosition 循环 */ @@ -1732,7 +1758,7 @@ watch([
- + @@ -1741,7 +1767,7 @@ watch([ - +
封装温度
@@ -1753,9 +1779,6 @@ watch([
功耗
{{ fmt(cpuPower, 1) }}W
- - -
@@ -1768,7 +1791,7 @@ watch([ - + @@ -1777,7 +1800,7 @@ watch([ - +
核心温度
@@ -1789,9 +1812,6 @@ watch([
功耗
{{ fmt(gpuPower, 1) }}W
- - -
@@ -1835,7 +1855,7 @@ watch([ - + @@ -1847,25 +1867,15 @@ watch([
下载
-
-
- {{ downSpeed.value }}{{ downSpeed.unit }} -
- - - +
+ {{ downSpeed.value }}{{ downSpeed.unit }}
上传
-
-
- {{ upSpeed.value }}{{ upSpeed.unit }} -
- - - +
+ {{ upSpeed.value }}{{ upSpeed.unit }}
@@ -2136,9 +2146,6 @@ watch([ {{ sensorLabel(item) }} / {{ hwLabel(item) }}
- - {{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }} - @@ -2651,78 +2658,7 @@ watch([
- - - - - 预览 - - - -
悬浮窗({{ osdLayoutLabel }})
-
- -
- -
- - -
-
-
{{ g.label }}
-
- - {{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }} - -
-
-
- - -
-
- {{ g.label }} - - {{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }} - -
-
-
-
-
+
diff --git a/src/modules/tray/TrayMenu.vue b/src/modules/tray/TrayMenu.vue new file mode 100644 index 0000000..1f2e719 --- /dev/null +++ b/src/modules/tray/TrayMenu.vue @@ -0,0 +1,726 @@ + + + + + + + + diff --git a/temp/snap2.json b/temp/snap2.json new file mode 100644 index 0000000..2062d94 --- /dev/null +++ b/temp/snap2.json @@ -0,0 +1 @@ +{"schemaVersion":1,"timestamp":1785393390698,"coldStartMs":346.1,"isAdmin":true,"ready":true,"groups":[{"id":"cpu","name":"CPU","sensors":[{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#1","name":"CPU Core #1","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":19.05,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#2","name":"CPU Core #2","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":9.52,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#3","name":"CPU Core #3","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":1.59,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#4","name":"CPU Core #4","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#5","name":"CPU Core #5","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":1.59,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#6","name":"CPU Core #6","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#7","name":"CPU Core #7","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":11.11,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#8","name":"CPU Core #8","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":3.17,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#9","name":"CPU Core #9","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":4.76,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#10","name":"CPU Core #10","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":6.35,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#11","name":"CPU Core #11","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#12","name":"CPU Core #12","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#13","name":"CPU Core #13","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":1.59,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#14","name":"CPU Core #14","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#15","name":"CPU Core #15","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_#16","name":"CPU Core #16","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":1.59,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_total","name":"CPU Total","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":3.77,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/load/cpu_core_max","name":"CPU Core Max","type":"load","hardwareName":"AMD Ryzen 7 9800X3D","value":19.05,"unit":"%"},{"id":"cpu/amd_ryzen_7_9800x3d/power/package","name":"Package","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/cores_(average)","name":"Cores (Average)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/cores_(average_effective)","name":"Cores (Average Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#1","name":"Core #1","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#1_(effective)","name":"Core #1 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#1_(smu)","name":"Core #1 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#2","name":"Core #2","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#2_(effective)","name":"Core #2 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#2_(smu)","name":"Core #2 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#3","name":"Core #3","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#3_(effective)","name":"Core #3 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#3_(smu)","name":"Core #3 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#4","name":"Core #4","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#4_(effective)","name":"Core #4 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#4_(smu)","name":"Core #4 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#5","name":"Core #5","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#5_(effective)","name":"Core #5 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#5_(smu)","name":"Core #5 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#6","name":"Core #6","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#6_(effective)","name":"Core #6 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#6_(smu)","name":"Core #6 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#7","name":"Core #7","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#7_(effective)","name":"Core #7 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#7_(smu)","name":"Core #7 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#8","name":"Core #8","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/clock/core_#8_(effective)","name":"Core #8 (Effective)","type":"clock","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"MHz"},{"id":"cpu/amd_ryzen_7_9800x3d/power/core_#8_(smu)","name":"Core #8 (SMU)","type":"power","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"W"},{"id":"cpu/amd_ryzen_7_9800x3d/temperature/core_(tctl/tdie)","name":"Core (Tctl/Tdie)","type":"temperature","hardwareName":"AMD Ryzen 7 9800X3D","value":0,"unit":"\u00B0C"}]},{"id":"memory","name":"Memory","sensors":[{"id":"memory/virtual_memory/data/memory_used","name":"Memory Used","type":"data","hardwareName":"Virtual Memory","value":29.933598,"unit":"GB"},{"id":"memory/virtual_memory/data/memory_available","name":"Memory Available","type":"data","hardwareName":"Virtual Memory","value":22.446678,"unit":"GB"},{"id":"memory/virtual_memory/load/memory","name":"Memory","type":"load","hardwareName":"Virtual Memory","value":57.146698,"unit":"%"},{"id":"memory/total_memory/data/memory_used","name":"Memory Used","type":"data","hardwareName":"Total Memory","value":12.919495,"unit":"GB"},{"id":"memory/total_memory/data/memory_available","name":"Memory Available","type":"data","hardwareName":"Total Memory","value":18.460781,"unit":"GB"},{"id":"memory/total_memory/load/memory","name":"Memory","type":"load","hardwareName":"Total Memory","value":41.17075,"unit":"%"}]},{"id":"storage","name":"Storage","sensors":[{"id":"storage/samsung_ssd_970_evo_plus_2tb/temperature/temperature","name":"Temperature","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":40,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/temperature/temperature_2","name":"Temperature 2","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":46.850006,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/temperature/temperature_warning","name":"Temperature warning","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":81,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/temperature/temperature_critical","name":"Temperature critical","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":84,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/data/data_read","name":"Data read","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":66645,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/data/data_written","name":"Data written","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":37186,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/load/used_space","name":"Used Space","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":-922153000,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/data/free_space","name":"Free Space","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":1.8446744E+10,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/data/total_space","name":"Total Space","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":2000.3989,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/load/read_activity","name":"Read Activity","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":0,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/load/write_activity","name":"Write Activity","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":0,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_2tb/load/total_activity","name":"Total Activity","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 2TB","value":0,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/temperature/temperature","name":"Temperature","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":43,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/temperature/temperature_2","name":"Temperature 2","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":43.850006,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/temperature/temperature_warning","name":"Temperature warning","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":84,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/temperature/temperature_critical","name":"Temperature critical","type":"temperature","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":84,"unit":"\u00B0C"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/data/data_read","name":"Data read","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":68692,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/data/data_written","name":"Data written","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":57143,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/load/used_space","name":"Used Space","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":63.86202,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/data/free_space","name":"Free Space","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":180.72888,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/data/total_space","name":"Total Space","type":"data","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":500.10785,"unit":"GB"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/load/read_activity","name":"Read Activity","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":0,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/load/write_activity","name":"Write Activity","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":0.041056305,"unit":"%"},{"id":"storage/samsung_ssd_970_evo_plus_500gb/load/total_activity","name":"Total Activity","type":"load","hardwareName":"Samsung SSD 970 EVO Plus 500GB","value":0,"unit":"%"},{"id":"storage/zss11da02c/temperature/temperature","name":"Temperature","type":"temperature","hardwareName":"ZSS11DA02C","value":32,"unit":"\u00B0C"},{"id":"storage/zss11da02c/load/used_space","name":"Used Space","type":"load","hardwareName":"ZSS11DA02C","value":43.86588,"unit":"%"},{"id":"storage/zss11da02c/data/free_space","name":"Free Space","type":"data","hardwareName":"ZSS11DA02C","value":287.46854,"unit":"GB"},{"id":"storage/zss11da02c/data/total_space","name":"Total Space","type":"data","hardwareName":"ZSS11DA02C","value":512.11017,"unit":"GB"},{"id":"storage/zss11da02c/load/read_activity","name":"Read Activity","type":"load","hardwareName":"ZSS11DA02C","value":0,"unit":"%"},{"id":"storage/zss11da02c/load/write_activity","name":"Write Activity","type":"load","hardwareName":"ZSS11DA02C","value":0,"unit":"%"},{"id":"storage/zss11da02c/load/total_activity","name":"Total Activity","type":"load","hardwareName":"ZSS11DA02C","value":0.03464339,"unit":"%"}]},{"id":"gpuamd","name":"GpuAmd","sensors":[{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_3d","name":"D3D 3D","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":14.1529455,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_copy","name":"D3D Copy","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":0,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_compute_0","name":"D3D Compute 0","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":0,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_copy","name":"D3D Copy","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":0.010940037,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_timer_0","name":"D3D Timer 0","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":0,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_security_1","name":"D3D Security 1","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":0,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_video_jpeg_0","name":"D3D Video JPEG 0","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":0,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/d3d_video_codec_engine","name":"D3D Video Codec Engine","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":10.0678215,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/power/gpu_package","name":"GPU Package","type":"power","hardwareName":"AMD Radeon RX 7900 XT","value":103,"unit":"W"},{"id":"gpuamd/amd_radeon_rx_7900_xt/clock/gpu_core","name":"GPU Core","type":"clock","hardwareName":"AMD Radeon RX 7900 XT","value":1444,"unit":"MHz"},{"id":"gpuamd/amd_radeon_rx_7900_xt/clock/gpu_memory","name":"GPU Memory","type":"clock","hardwareName":"AMD Radeon RX 7900 XT","value":2487,"unit":"MHz"},{"id":"gpuamd/amd_radeon_rx_7900_xt/temperature/gpu_core","name":"GPU Core","type":"temperature","hardwareName":"AMD Radeon RX 7900 XT","value":54,"unit":"\u00B0C"},{"id":"gpuamd/amd_radeon_rx_7900_xt/temperature/gpu_memory","name":"GPU Memory","type":"temperature","hardwareName":"AMD Radeon RX 7900 XT","value":68,"unit":"\u00B0C"},{"id":"gpuamd/amd_radeon_rx_7900_xt/temperature/gpu_hot_spot","name":"GPU Hot Spot","type":"temperature","hardwareName":"AMD Radeon RX 7900 XT","value":62,"unit":"\u00B0C"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/gpu_core","name":"GPU Core","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":34,"unit":"%"},{"id":"gpuamd/amd_radeon_rx_7900_xt/load/gpu_memory","name":"GPU Memory","type":"load","hardwareName":"AMD Radeon RX 7900 XT","value":5,"unit":"%"}]}]} \ No newline at end of file diff --git a/temp/snapshot.json b/temp/snapshot.json new file mode 100644 index 0000000..3e0b7fe --- /dev/null +++ b/temp/snapshot.json @@ -0,0 +1 @@ +{"error":"internal","message":".NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://docs.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling)."} \ No newline at end of file