mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-05-18 04:56:47 +08:00
✨ 支持托盘 (#181)
* Initial plan
* Add system tray icon feature with show/quit menu
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Apply Rust code formatting
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Address code review feedback: improve documentation and reduce duplication
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Make tray behavior configurable - add closeToTray setting
- Add closeToTray setting in app store (default: false)
- Add UI toggle in PageConfig.vue for "关闭到托盘" setting
- Modify window close handler to emit event instead of always hiding
- Add hide_main_window and quit_app commands in backend
- Frontend listens for close event and respects user preference
- When closeToTray is enabled, window minimizes to tray on close
- When disabled, application quits on close (original behavior)
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Remove duplicate SUB_WINDOW_LABELS constant - use shared constant from lib.rs
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Fix tray icon issues: remove duplicate tray, add icon loading, close sub-windows on hide
1. Remove duplicate tray icon: Removed trayIcon config from tauri.conf.json (was creating 2 trays)
2. Add proper icon loading: Use image crate to decode PNG and create RGBA icon data
3. Close sub-windows when hiding: Modified hide_main_window to close all sub-windows before hiding
4. Sync with master branch: Merged latest commit c8fe03c
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Address code review: remove anyhow, add separator, improve comments
1. Remove anyhow dependency - Use Tauri's InvalidIcon error instead
2. Add menu separator between "显示窗口" and "退出应用"
3. Add comment explaining image crate necessity (Tauri Image only accepts raw RGBA)
4. Keep HTTPS URLs with note about SSH preference for local development
5. Image crate remains necessary as Tauri doesn't provide PNG decoding
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Revert to SSH URLs for plugin dependencies as requested
Changed all tauri-plugin git URLs from HTTPS to SSH format.
Project CI has SSH configuration, so SSH is the preferred method.
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
* Use storeToRefs for closeToTray property
Changed closeToTray access to use storeToRefs in both App.vue and PageConfig.vue
for better reactivity and consistency with other store properties.
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com>
This commit is contained in:
@@ -127,3 +127,31 @@ pub fn is_in_admin() -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏主窗口到托盘
|
||||
#[tauri::command]
|
||||
pub async fn hide_main_window(app_handle: AppHandle) {
|
||||
// 关闭所有子窗口
|
||||
for label in crate::SUB_WINDOW_LABELS.iter() {
|
||||
if let Some(sub) = app_handle.get_webview_window(label) {
|
||||
let _ = sub.destroy();
|
||||
}
|
||||
}
|
||||
// 隐藏主窗口
|
||||
if let Some(window) = app_handle.get_webview_window("TeyvatGuide") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 退出应用
|
||||
#[tauri::command]
|
||||
pub async fn quit_app(app_handle: AppHandle) {
|
||||
// 关闭所有子窗口
|
||||
for label in crate::SUB_WINDOW_LABELS.iter() {
|
||||
if let Some(sub) = app_handle.get_webview_window(label) {
|
||||
let _ = sub.destroy();
|
||||
}
|
||||
}
|
||||
// 退出应用
|
||||
app_handle.exit(0);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
mod client;
|
||||
mod commands;
|
||||
mod plugins;
|
||||
mod tray;
|
||||
mod utils;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod watchdog;
|
||||
@@ -11,9 +12,14 @@ mod watchdog;
|
||||
mod yae;
|
||||
|
||||
use crate::client::create_mhy_client;
|
||||
use crate::commands::{create_window, execute_js, get_dir_size, init_app, is_in_admin};
|
||||
use crate::commands::{
|
||||
create_window, execute_js, get_dir_size, hide_main_window, init_app, is_in_admin, quit_app,
|
||||
};
|
||||
use crate::plugins::{build_log_plugin, build_si_plugin};
|
||||
use tauri::{generate_context, generate_handler, Manager, Window, WindowEvent};
|
||||
use tauri::{generate_context, generate_handler, Emitter, Manager, Window, WindowEvent};
|
||||
|
||||
// 子窗口 label 的数组
|
||||
pub const SUB_WINDOW_LABELS: [&str; 3] = ["Sub_window", "Dev_JSON", "mhy_client"];
|
||||
|
||||
// 窗口事件处理
|
||||
fn window_event_handler(app: &Window, event: &WindowEvent) {
|
||||
@@ -21,16 +27,12 @@ fn window_event_handler(app: &Window, event: &WindowEvent) {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
if app.label() == "TeyvatGuide" {
|
||||
// 子窗口 label 的数组
|
||||
const SUB_WINDOW_LABELS: [&str; 3] = ["Sub_window", "Dev_JSON", "mhy_client"];
|
||||
for label in SUB_WINDOW_LABELS.iter() {
|
||||
let sub = app.get_webview_window(label);
|
||||
if sub.is_some() {
|
||||
sub.unwrap().destroy().unwrap();
|
||||
}
|
||||
}
|
||||
// 主窗口:发送事件让前端根据配置决定是隐藏还是退出
|
||||
let _ = app.emit("main-window-close-requested", ());
|
||||
} else {
|
||||
// 子窗口:直接销毁
|
||||
app.destroy().unwrap();
|
||||
}
|
||||
app.destroy().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -78,6 +80,9 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.plugin(build_log_plugin())
|
||||
.setup(|_app| {
|
||||
// 创建系统托盘图标
|
||||
tray::create_tray(_app.handle())
|
||||
.expect("Failed to initialize system tray icon. Please check if the tray icon file exists and the system supports tray icons.");
|
||||
let _window = _app.get_webview_window("TeyvatGuide");
|
||||
#[cfg(debug_assertions)]
|
||||
if _window.is_some() {
|
||||
@@ -92,6 +97,8 @@ pub fn run() {
|
||||
get_dir_size,
|
||||
create_mhy_client,
|
||||
is_in_admin,
|
||||
hide_main_window,
|
||||
quit_app,
|
||||
#[cfg(target_os = "windows")]
|
||||
yae::call_yae_dll,
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
78
src-tauri/src/tray.rs
Normal file
78
src-tauri/src/tray.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! @file src/tray.rs
|
||||
//! @desc 系统托盘模块,负责创建和管理系统托盘图标
|
||||
//! @since Beta v0.8.8
|
||||
|
||||
use tauri::image::Image;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
/// 创建系统托盘图标
|
||||
pub fn create_tray<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<()> {
|
||||
// 创建托盘菜单
|
||||
let show_item = MenuItemBuilder::with_id("show", "显示窗口").build(app)?;
|
||||
let separator = PredefinedMenuItem::separator(app)?;
|
||||
let quit_item = MenuItemBuilder::with_id("quit", "退出应用").build(app)?;
|
||||
|
||||
let menu = MenuBuilder::new(app).item(&show_item).item(&separator).item(&quit_item).build()?;
|
||||
|
||||
// 加载托盘图标
|
||||
// 在不同操作系统上,托盘图标的显示效果可能有所不同:
|
||||
// - Windows: 使用 .ico 格式获得最佳效果
|
||||
// - macOS: 支持 .icns 和 .png 格式
|
||||
// - Linux: 通常使用 .png 格式
|
||||
let icon_bytes = include_bytes!("../icons/32x32.png");
|
||||
|
||||
// 使用 image crate 解码 PNG 图标
|
||||
// Tauri 的 Image 结构只接受原始 RGBA 数据,因此需要使用 image crate 解码 PNG
|
||||
let img = image::load_from_memory(icon_bytes).map_err(|e| {
|
||||
tauri::Error::InvalidIcon(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
})?;
|
||||
let rgba = img.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
let icon = Image::new_owned(rgba.into_raw(), width, height);
|
||||
|
||||
// 创建托盘图标并设置事件处理
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.icon(icon)
|
||||
.tooltip("Teyvat Guide")
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| {
|
||||
match event.id.as_ref() {
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("TeyvatGuide") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
// 关闭所有子窗口
|
||||
for label in crate::SUB_WINDOW_LABELS.iter() {
|
||||
if let Some(sub) = app.get_webview_window(label) {
|
||||
let _ = sub.destroy();
|
||||
}
|
||||
}
|
||||
// 退出应用
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("TeyvatGuide") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user