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:
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, .. } => {
|
||||||
api.prevent_close();
|
let settings = crate::settings::get_settings();
|
||||||
let _ = window.hide();
|
|
||||||
|
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 {
|
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();
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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> {
|
||||||
vec![
|
vec![
|
||||||
"Code", // VS Code Stable
|
"Code", // VS Code Stable
|
||||||
"Code - Insiders", // VS Code Insiders
|
"Code - Insiders", // VS Code Insiders
|
||||||
"VSCodium", // VSCodium
|
"VSCodium", // VSCodium
|
||||||
"Code - OSS", // OSS 发行版
|
"Code - OSS", // OSS 发行版
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
<label className="flex items-center justify-between">
|
<div className="space-y-3">
|
||||||
<span className="text-sm text-gray-500">
|
<label className="flex items-center justify-between">
|
||||||
在菜单栏显示图标(系统托盘)
|
<div>
|
||||||
</span>
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||||
<input
|
关闭时最小化到托盘
|
||||||
type="checkbox"
|
</span>
|
||||||
checked={settings.showInTray}
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
onChange={(e) =>
|
勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。
|
||||||
setSettings({ ...settings, showInTray: e.target.checked })
|
</p>
|
||||||
}
|
</div>
|
||||||
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
|
<input
|
||||||
/>
|
type="checkbox"
|
||||||
</label>
|
checked={settings.minimizeToTrayOnClose}
|
||||||
</div> */}
|
onChange={(e) =>
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
minimizeToTrayOnClose: e.target.checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* VS Code 自动同步设置已移除 */}
|
{/* VS Code 自动同步设置已移除 */}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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 配置目录(可选)
|
||||||
|
|||||||
Reference in New Issue
Block a user