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", "core:default",
"opener:default", "opener:default",
"updater:default", "updater:default",
"core:window:allow-set-skip-taskbar",
"process:allow-restart", "process:allow-restart",
"dialog:default" "dialog:default"
] ]

View File

@@ -2,8 +2,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use tauri::State; use tauri::State;
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_dialog::DialogExt; use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
use crate::app_config::AppType; use crate::app_config::AppType;
use crate::codex_config; use crate::codex_config;
@@ -655,9 +655,8 @@ pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result<bool, St
/// 获取设置 /// 获取设置
#[tauri::command] #[tauri::command]
pub async fn get_settings() -> Result<serde_json::Value, String> { pub async fn get_settings() -> Result<crate::settings::AppSettings, String> {
serde_json::to_value(crate::settings::get_settings()) Ok(crate::settings::get_settings())
.map_err(|e| format!("序列化设置失败: {}", e))
} }
/// 保存设置 /// 保存设置
@@ -697,11 +696,17 @@ pub async fn is_portable_mode() -> Result<bool, String> {
#[tauri::command] #[tauri::command]
pub async fn get_vscode_settings_status() -> Result<ConfigStatus, String> { pub async fn get_vscode_settings_status() -> Result<ConfigStatus, String> {
if let Some(p) = vscode::find_existing_settings() { 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 { } else {
// 默认返回 macOS 稳定版路径(或其他平台首选项的第一个候选),但标记不存在 // 默认返回 macOS 稳定版路径(或其他平台首选项的第一个候选),但标记不存在
let preferred = vscode::candidate_settings_paths().into_iter().next(); 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 { match event_id {
"show_main" => { "show_main" => {
if let Some(window) = app.get_webview_window("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.unminimize();
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
@@ -241,23 +245,31 @@ pub fn run() {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{ {
builder = builder.plugin(tauri_plugin_single_instance::init( builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize(); let _ = window.unminimize();
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
} }
}, }));
));
} }
let builder = builder let builder = builder
// 拦截窗口关闭:仅隐藏窗口,保持进程与托盘常驻 // 拦截窗口关闭:根据设置决定是否最小化到托盘
.on_window_event(|window, event| match event { .on_window_event(|window, event| match event {
tauri::WindowEvent::CloseRequested { api, .. } => { tauri::WindowEvent::CloseRequested { api, .. } => {
let settings = crate::settings::get_settings();
if settings.minimize_to_tray_on_close {
api.prevent_close(); api.prevent_close();
let _ = window.hide(); 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 { match event {
RunEvent::Reopen { .. } => { RunEvent::Reopen { .. } => {
if let Some(window) = app_handle.get_webview_window("main") { 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.unminimize();
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();

View File

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

View File

@@ -1,4 +1,4 @@
use std::path::{PathBuf}; use std::path::PathBuf;
/// 枚举可能的 VS Code 发行版配置目录名称 /// 枚举可能的 VS Code 发行版配置目录名称
fn vscode_product_dirs() -> Vec<&'static str> { 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() { if let Some(home) = dirs::home_dir() {
for prod in vscode_product_dirs() { for prod in vscode_product_dirs() {
paths.push( 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) { export default function SettingsModal({ onClose }: SettingsModalProps) {
const [settings, setSettings] = useState<Settings>({ const [settings, setSettings] = useState<Settings>({
showInTray: true, showInTray: true,
minimizeToTrayOnClose: true,
claudeConfigDir: undefined, claudeConfigDir: undefined,
codexConfigDir: undefined, codexConfigDir: undefined,
}); });
@@ -65,8 +66,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
(loadedSettings as any)?.showInTray ?? (loadedSettings as any)?.showInTray ??
(loadedSettings as any)?.showInDock ?? (loadedSettings as any)?.showInDock ??
true; true;
const minimizeToTrayOnClose =
(loadedSettings as any)?.minimizeToTrayOnClose ??
(loadedSettings as any)?.minimize_to_tray_on_close ??
true;
setSettings({ setSettings({
showInTray, showInTray,
minimizeToTrayOnClose,
claudeConfigDir: claudeConfigDir:
typeof (loadedSettings as any)?.claudeConfigDir === "string" typeof (loadedSettings as any)?.claudeConfigDir === "string"
? (loadedSettings as any).claudeConfigDir ? (loadedSettings as any).claudeConfigDir
@@ -305,26 +311,35 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 设置内容 */} {/* 设置内容 */}
<div className="px-6 py-4 space-y-6"> <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 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
显示设置(系统托盘)
</h3> </h3>
<div className="space-y-3">
<label className="flex items-center justify-between"> <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> </span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
退
</p>
</div>
<input <input
type="checkbox" type="checkbox"
checked={settings.showInTray} checked={settings.minimizeToTrayOnClose}
onChange={(e) => 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" className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
/> />
</label> </label>
</div> */} </div>
</div>
{/* VS Code 自动同步设置已移除 */} {/* VS Code 自动同步设置已移除 */}

View File

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

View File

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