feat: support minimizing window to tray on close (#41)

fix: grant set_skip_taskbar permission through default capability

chore: align settings casing and defaults between Rust and frontend

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
ShaSan
2025-09-26 20:18:11 +08:00
committed by GitHub
parent 11ee8bddf7
commit 5d2d15690c
8 changed files with 95 additions and 51 deletions

View File

@@ -9,6 +9,7 @@
"core:default",
"opener:default",
"updater:default",
"core:window:allow-set-skip-taskbar",
"process:allow-restart",
"dialog:default"
]

View File

@@ -2,8 +2,8 @@
use std::collections::HashMap;
use tauri::State;
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
use crate::app_config::AppType;
use crate::codex_config;
@@ -655,9 +655,8 @@ pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result<bool, St
/// 获取设置
#[tauri::command]
pub async fn get_settings() -> Result<serde_json::Value, String> {
serde_json::to_value(crate::settings::get_settings())
.map_err(|e| format!("序列化设置失败: {}", e))
pub async fn get_settings() -> Result<crate::settings::AppSettings, String> {
Ok(crate::settings::get_settings())
}
/// 保存设置
@@ -697,11 +696,17 @@ pub async fn is_portable_mode() -> Result<bool, String> {
#[tauri::command]
pub async fn get_vscode_settings_status() -> Result<ConfigStatus, String> {
if let Some(p) = vscode::find_existing_settings() {
Ok(ConfigStatus { exists: true, path: p.to_string_lossy().to_string() })
Ok(ConfigStatus {
exists: true,
path: p.to_string_lossy().to_string(),
})
} else {
// 默认返回 macOS 稳定版路径(或其他平台首选项的第一个候选),但标记不存在
let preferred = vscode::candidate_settings_paths().into_iter().next();
Ok(ConfigStatus { exists: false, path: preferred.unwrap_or_default().to_string_lossy().to_string() })
Ok(ConfigStatus {
exists: false,
path: preferred.unwrap_or_default().to_string_lossy().to_string(),
})
}
}

View File

@@ -123,6 +123,10 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
match event_id {
"show_main" => {
if let Some(window) = app.get_webview_window("main") {
#[cfg(target_os = "windows")]
{
let _ = window.set_skip_taskbar(false);
}
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
@@ -241,23 +245,31 @@ pub fn run() {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{
builder = builder.plugin(tauri_plugin_single_instance::init(
|app, _args, _cwd| {
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
},
));
}));
}
let builder = builder
// 拦截窗口关闭:仅隐藏窗口,保持进程与托盘常驻
// 拦截窗口关闭:根据设置决定是否最小化到托盘
.on_window_event(|window, event| match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
let settings = crate::settings::get_settings();
if settings.minimize_to_tray_on_close {
api.prevent_close();
let _ = window.hide();
#[cfg(target_os = "windows")]
{
let _ = window.set_skip_taskbar(true);
}
} else {
window.app_handle().exit(0);
}
}
_ => {}
})
@@ -394,6 +406,10 @@ pub fn run() {
match event {
RunEvent::Reopen { .. } => {
if let Some(window) = app_handle.get_webview_window("main") {
#[cfg(target_os = "windows")]
{
let _ = window.set_skip_taskbar(false);
}
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();

View File

@@ -9,6 +9,8 @@ use std::sync::{OnceLock, RwLock};
pub struct AppSettings {
#[serde(default = "default_show_in_tray")]
pub show_in_tray: bool,
#[serde(default = "default_minimize_to_tray_on_close")]
pub minimize_to_tray_on_close: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claude_config_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -19,10 +21,15 @@ fn default_show_in_tray() -> bool {
true
}
fn default_minimize_to_tray_on_close() -> bool {
true
}
impl Default for AppSettings {
fn default() -> Self {
Self {
show_in_tray: true,
minimize_to_tray_on_close: true,
claude_config_dir: None,
codex_config_dir: None,
}
@@ -78,8 +85,7 @@ impl AppSettings {
let path = Self::settings_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("创建设置目录失败: {}", e))?;
fs::create_dir_all(parent).map_err(|e| format!("创建设置目录失败: {}", e))?;
}
let json = serde_json::to_string_pretty(&normalized)
@@ -113,19 +119,14 @@ fn resolve_override_path(raw: &str) -> PathBuf {
}
pub fn get_settings() -> AppSettings {
settings_store()
.read()
.expect("读取设置锁失败")
.clone()
settings_store().read().expect("读取设置锁失败").clone()
}
pub fn update_settings(mut new_settings: AppSettings) -> Result<(), String> {
new_settings.normalize_paths();
new_settings.save()?;
let mut guard = settings_store()
.write()
.expect("写入设置锁失败");
let mut guard = settings_store().write().expect("写入设置锁失败");
*guard = new_settings;
Ok(())
}

View File

@@ -1,4 +1,4 @@
use std::path::{PathBuf};
use std::path::PathBuf;
/// 枚举可能的 VS Code 发行版配置目录名称
fn vscode_product_dirs() -> Vec<&'static str> {
@@ -19,7 +19,11 @@ pub fn candidate_settings_paths() -> Vec<PathBuf> {
if let Some(home) = dirs::home_dir() {
for prod in vscode_product_dirs() {
paths.push(
home.join("Library").join("Application Support").join(prod).join("User").join("settings.json")
home.join("Library")
.join("Application Support")
.join(prod)
.join("User")
.join("settings.json"),
);
}
}

View File

@@ -25,6 +25,7 @@ interface SettingsModalProps {
export default function SettingsModal({ onClose }: SettingsModalProps) {
const [settings, setSettings] = useState<Settings>({
showInTray: true,
minimizeToTrayOnClose: true,
claudeConfigDir: undefined,
codexConfigDir: undefined,
});
@@ -65,8 +66,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
(loadedSettings as any)?.showInTray ??
(loadedSettings as any)?.showInDock ??
true;
const minimizeToTrayOnClose =
(loadedSettings as any)?.minimizeToTrayOnClose ??
(loadedSettings as any)?.minimize_to_tray_on_close ??
true;
setSettings({
showInTray,
minimizeToTrayOnClose,
claudeConfigDir:
typeof (loadedSettings as any)?.claudeConfigDir === "string"
? (loadedSettings as any).claudeConfigDir
@@ -305,26 +311,35 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 设置内容 */}
<div className="px-6 py-4 space-y-6">
{/* 系统托盘设置(未实现)
说明:此开关用于控制是否在系统托盘/菜单栏显示应用图标。 */}
{/* <div>
{/* 窗口行为设置 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
显示设置(系统托盘)
</h3>
<div className="space-y-3">
<label className="flex items-center justify-between">
<span className="text-sm text-gray-500">
在菜单栏显示图标(系统托盘)
<div>
<span className="text-sm text-gray-900 dark:text-gray-100">
</span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
退
</p>
</div>
<input
type="checkbox"
checked={settings.showInTray}
checked={settings.minimizeToTrayOnClose}
onChange={(e) =>
setSettings({ ...settings, showInTray: e.target.checked })
setSettings((prev) => ({
...prev,
minimizeToTrayOnClose: e.target.checked,
}))
}
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
/>
</label>
</div> */}
</div>
</div>
{/* VS Code 自动同步设置已移除 */}

View File

@@ -223,7 +223,7 @@ export const tauriAPI = {
return await invoke("get_settings");
} catch (error) {
console.error("获取设置失败:", error);
return { showInTray: true };
return { showInTray: true, minimizeToTrayOnClose: true };
}
},

View File

@@ -24,6 +24,8 @@ export interface AppConfig {
export interface Settings {
// 是否在系统托盘macOS 菜单栏)显示图标
showInTray: boolean;
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
minimizeToTrayOnClose: boolean;
// 覆盖 Claude Code 配置目录(可选)
claudeConfigDir?: string;
// 覆盖 Codex 配置目录(可选)