feat(settings): add dock icon visibility setting to desktop settings

- Introduced a new setting `show_dock_icon` in the `DesktopSettings` interface.
- Updated `AppLifecycleSettingsCard` to manage dock icon visibility with a toggle switch.
- Enhanced the API to read and write the new dock icon visibility setting.
- Added corresponding translations for the new setting in both English and Chinese locales.
- Updated tests to ensure the default behavior of showing the dock icon and proper round-trip functionality for settings.
This commit is contained in:
leokun
2026-08-27 11:24:04 +08:00
parent 31dfa8d295
commit 82f3ae8d59
12 changed files with 211 additions and 82 deletions
@@ -16,6 +16,7 @@
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized",
"core:window:allow-close",
"core:app:allow-set-dock-visibility",
"allow-open-terminal-with-command",
"clipboard-manager:allow-write-text",
"autostart:default",
+7 -4
View File
@@ -186,9 +186,12 @@ pub fn run() {
let listener = tauri::async_runtime::block_on(server.bind())?;
let address = listener.local_addr()?;
tauri::async_runtime::block_on(server.harness().cleanup_stale_settings())?;
let silent_start = tauri::async_runtime::block_on(server.store().desktop_settings())
.map(|settings| settings.silent_start)
.unwrap_or(false);
let desktop_settings =
tauri::async_runtime::block_on(server.store().desktop_settings())
.unwrap_or_default();
#[cfg(target_os = "macos")]
app.handle()
.set_dock_visibility(desktop_settings.show_dock_icon)?;
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
let app_handle = app.handle().clone();
@@ -206,7 +209,7 @@ pub fn run() {
exiting: AtomicBool::new(false),
});
let window = create_main_window(app.handle(), address)?;
if silent_start && started_by_autostart {
if desktop_settings.silent_start && started_by_autostart {
tracing::info!("silent autostart enabled; keeping the main window hidden");
} else {
window.show()?;
+1
View File
@@ -143,6 +143,7 @@ export interface TabSettings {
export interface DesktopSettings {
silent_start: boolean;
show_dock_icon: boolean;
}
export interface OverviewMetrics {
@@ -1,10 +1,12 @@
import { useEffect, useState } from "react";
import {
currentAppVersion,
hasDockVisibilitySetting,
hasNativeAppLifecycle,
readAutostart,
readSilentStart,
readDesktopSettings,
writeAutostart,
writeDockIconVisibility,
writeSilentStart,
} from "../../native/appLifecycle";
import { updateStore, useUpdateStore } from "../../store/updateStore";
@@ -17,12 +19,14 @@ import styles from "./AppLifecycleSettingsCard.module.scss";
export function AppLifecycleSettingsCard() {
const message = useMessage();
const native = hasNativeAppLifecycle();
const dockVisibilitySetting = hasDockVisibilitySetting();
const { availableVersion, checking, installing } = useUpdateStore();
const [version, setVersion] = useState("…");
const [autostart, setAutostart] = useState(false);
const [loadingAutostart, setLoadingAutostart] = useState(native);
const [silentStart, setSilentStart] = useState(false);
const [loadingSilentStart, setLoadingSilentStart] = useState(native);
const [dockIconVisible, setDockIconVisible] = useState(true);
const [loadingDesktopSettings, setLoadingDesktopSettings] = useState(native);
useEffect(() => {
let disposed = false;
@@ -32,10 +36,17 @@ export function AppLifecycleSettingsCard() {
.then((enabled) => { if (!disposed) setAutostart(enabled); })
.catch((cause) => message(cause instanceof Error ? cause.message : String(cause)))
.finally(() => { if (!disposed) setLoadingAutostart(false); });
void readSilentStart()
.then((silent) => { if (!disposed) setSilentStart(silent); })
void readDesktopSettings()
.then((settings) => {
if (disposed) return;
setSilentStart(settings.silent_start);
setDockIconVisible(settings.show_dock_icon);
})
.catch(() => {})
.finally(() => { if (!disposed) setLoadingSilentStart(false); });
.finally(() => {
if (disposed) return;
setLoadingDesktopSettings(false);
});
}
return () => { disposed = true; };
}, [message, native]);
@@ -55,14 +66,27 @@ export function AppLifecycleSettingsCard() {
const toggleSilentStart = async (enabled: boolean) => {
try {
setLoadingSilentStart(true);
setLoadingDesktopSettings(true);
await writeSilentStart(enabled);
setSilentStart(await readSilentStart());
setSilentStart(enabled);
message(enabled ? t("已开启静默启动") : t("已关闭静默启动"));
} catch (cause) {
message(cause instanceof Error ? cause.message : String(cause));
} finally {
setLoadingSilentStart(false);
setLoadingDesktopSettings(false);
}
};
const toggleDockIcon = async (visible: boolean) => {
try {
setLoadingDesktopSettings(true);
await writeDockIconVisibility(visible);
setDockIconVisible(visible);
message(visible ? t("已显示 Dock 栏图标") : t("已隐藏 Dock 栏图标"));
} catch (cause) {
message(cause instanceof Error ? cause.message : String(cause));
} finally {
setLoadingDesktopSettings(false);
}
};
@@ -103,11 +127,23 @@ export function AppLifecycleSettingsCard() {
</div>
<Switch
checked={silentStart}
disabled={!native || loadingSilentStart}
disabled={!native || loadingDesktopSettings}
label={t("静默启动")}
onChange={(enabled) => void toggleSilentStart(enabled)}
/>
</div>}
{dockVisibilitySetting && <div className={styles.row}>
<div>
<strong>{t("在 Dock 栏显示")}</strong>
<small>{t("关闭后隐藏 Dock 栏图标,仍可通过菜单栏图标打开应用。")}</small>
</div>
<Switch
checked={dockIconVisible}
disabled={loadingDesktopSettings}
label={t("在 Dock 栏显示")}
onChange={(visible) => void toggleDockIcon(visible)}
/>
</div>}
<div className={styles.row}>
<div>
<strong>{t("软件更新")}</strong>
+77 -48
View File
@@ -516,7 +516,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 120,
"line": 156,
"column": 27
}
]
@@ -735,7 +735,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 72,
"line": 96,
"column": 29
}
]
@@ -773,7 +773,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 116,
"line": 152,
"column": 13
}
]
@@ -1095,18 +1095,6 @@
}
]
},
"393df9bb13ea4900": {
"source": "命中",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/metrics/CacheHitRateChart.tsx",
"line": 60,
"column": 14
}
]
},
"393e1241552b1870": {
"source": "请求",
"kind": "text",
@@ -1196,6 +1184,18 @@
}
]
},
"3b67824289b5fa1e": {
"source": "已隐藏 Dock 栏图标",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 85,
"column": 45
}
]
},
"3c94b4c75940c178": {
"source": "输入 Token",
"kind": "text",
@@ -1392,7 +1392,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 102,
"line": 126,
"column": 17
}
]
@@ -1432,7 +1432,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 115,
"line": 151,
"column": 13
}
]
@@ -1868,7 +1868,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 48,
"line": 59,
"column": 40
}
]
@@ -2004,7 +2004,7 @@
"refs": [
{
"file": "api.ts",
"line": 314,
"line": 315,
"column": 43
}
]
@@ -2016,7 +2016,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 113,
"line": 149,
"column": 18
}
]
@@ -2095,6 +2095,23 @@
}
]
},
"680680288a6d2ad2": {
"source": "在 Dock 栏显示",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 137,
"column": 18
},
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 143,
"column": 16
}
]
},
"68ad603fafe4e0d6": {
"source": "导入旧版配置",
"kind": "text",
@@ -2189,7 +2206,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 72,
"line": 96,
"column": 78
}
]
@@ -2280,7 +2297,7 @@
"refs": [
{
"file": "api.ts",
"line": 309,
"line": 310,
"column": 43
}
]
@@ -2395,7 +2412,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 124,
"line": 160,
"column": 37
}
]
@@ -2407,7 +2424,7 @@
"refs": [
{
"file": "api.ts",
"line": 258,
"line": 259,
"column": 21
}
]
@@ -2446,12 +2463,12 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 89,
"line": 113,
"column": 18
},
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 95,
"line": 119,
"column": 16
}
]
@@ -2475,7 +2492,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 61,
"line": 72,
"column": 25
}
]
@@ -2810,7 +2827,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 61,
"line": 72,
"column": 40
}
]
@@ -2896,7 +2913,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 86,
"line": 110,
"column": 29
}
]
@@ -2908,12 +2925,12 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 101,
"line": 125,
"column": 18
},
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 107,
"line": 131,
"column": 16
}
]
@@ -3142,7 +3159,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 48,
"line": 59,
"column": 25
}
]
@@ -3166,7 +3183,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 120,
"line": 156,
"column": 39
}
]
@@ -3199,6 +3216,18 @@
}
]
},
"aecb952b1e6cce36": {
"source": "关闭后隐藏 Dock 栏图标,仍可通过菜单栏图标打开应用。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 138,
"column": 17
}
]
},
"aee88743413144a2": {
"source": "刷新",
"kind": "text",
@@ -3308,6 +3337,18 @@
}
]
},
"b6725f218ebaef26": {
"source": "已显示 Dock 栏图标",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 85,
"column": 25
}
]
},
"b75a46aad3e7c132": {
"source": "非缓存输入:{tokens}",
"kind": "template",
@@ -3493,7 +3534,7 @@
"refs": [
{
"file": "components/metrics/CacheHitRateChart.tsx",
"line": 71,
"line": 58,
"column": 62
}
]
@@ -3724,7 +3765,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 90,
"line": 114,
"column": 17
}
]
@@ -4323,18 +4364,6 @@
}
]
},
"f3a76d896853c1df": {
"source": "未命中",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/metrics/CacheHitRateChart.tsx",
"line": 60,
"column": 23
}
]
},
"f4694c46b1e19602": {
"source": "最终请求类型",
"kind": "text",
@@ -4493,7 +4522,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 124,
"line": 160,
"column": 25
}
]
+4 -2
View File
@@ -71,7 +71,6 @@
"37125ef2e1d707cb": "Server address or complete request URL, API Key, model name, display name, and note are required",
"378bb0eec39fa8a2": "Last page",
"37cb98ff4d5dcfcc": "Successful {successful} / failed {failed}",
"393df9bb13ea4900": "Hit",
"393e1241552b1870": "Request",
"398f8e6c6f0a0b97": "Continue selecting or typing",
"39f52eee100131d7": "Cached input",
@@ -79,6 +78,7 @@
"3a3f595df70ec8ff": "Clear storage",
"3a5040b68abf75f9": "Select all",
"3a8c76b2ce785f96": "Review and import",
"3b67824289b5fa1e": "Dock icon hidden",
"3c94b4c75940c178": "Input Tokens",
"3cfae5728b92b334": "Token usage: {tokens}",
"3d13868593ae4eeb": "Display language",
@@ -143,6 +143,7 @@
"656ab25e264cc4e4": "No models are available to Cursor yet",
"65a6318e07ec1e07": "Tools",
"65cb9a7b4f620b6b": "Prompt {tokens}",
"680680288a6d2ad2": "Show in Dock",
"68ad603fafe4e0d6": "Import legacy configuration",
"68ea5dd4d7af20e6": "System settings",
"6a9906c79f26c0ba": "Start time",
@@ -222,12 +223,14 @@
"ac69f68b7010ec79": "Download and install",
"ad6a60ee93d3ba3e": "Loading call details…",
"ae2d0b7f79cea4a3": "Model output: {tokens} × ${price}/1M = {cost}",
"aecb952b1e6cce36": "Hide the Dock icon when disabled. You can still open the app from the menu bar icon.",
"aee88743413144a2": "Refresh",
"b06325c5660f0c29": "Direct",
"b16c3b2ecedd6fe1": "Cursor integration is active. Add a model configuration to use a BYOK model.",
"b4411558b932266f": "Provider type",
"b502b1d414664337": "Prompt: {tokens}",
"b5141d3d19e9a048": "Yes",
"b6725f218ebaef26": "Dock icon shown",
"b75a46aad3e7c132": "Non-cached input: {tokens}",
"b79354009c614ae9": "Statistics",
"b86967982067d295": " (estimated)",
@@ -303,7 +306,6 @@
"ee239f3943293f87": "Sunday",
"ee6b89a6a740a4c4": "If a port is occupied, a new random port is selected and saved automatically. Restart the app after changing these settings.",
"f2bdc88464c51c2e": "Show API Key",
"f3a76d896853c1df": "Miss",
"f4694c46b1e19602": "Final request type",
"f4dcb6a3ceb32247": "Page {page} of {count}",
"f4fa9f31ea2ae58d": "Token usage calendar for the past year",
+4 -2
View File
@@ -71,7 +71,6 @@
"37125ef2e1d707cb": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空",
"378bb0eec39fa8a2": "最后一页",
"37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}",
"393df9bb13ea4900": "命中",
"393e1241552b1870": "请求",
"398f8e6c6f0a0b97": "继续选择或输入",
"39f52eee100131d7": "缓存输入",
@@ -79,6 +78,7 @@
"3a3f595df70ec8ff": "清理存储空间",
"3a5040b68abf75f9": "全选",
"3a8c76b2ce785f96": "查看并导入",
"3b67824289b5fa1e": "已隐藏 Dock 栏图标",
"3c94b4c75940c178": "输入 Token",
"3cfae5728b92b334": "Token 用量:{tokens}",
"3d13868593ae4eeb": "界面语言",
@@ -143,6 +143,7 @@
"656ab25e264cc4e4": "还没有可供 Cursor 使用的模型",
"65a6318e07ec1e07": "工具数",
"65cb9a7b4f620b6b": "提示词 {tokens}",
"680680288a6d2ad2": "在 Dock 栏显示",
"68ad603fafe4e0d6": "导入旧版配置",
"68ea5dd4d7af20e6": "系统设置",
"6a9906c79f26c0ba": "开始时间",
@@ -222,12 +223,14 @@
"ac69f68b7010ec79": "下载并安装",
"ad6a60ee93d3ba3e": "正在加载调用详情…",
"ae2d0b7f79cea4a3": "模型输出:{tokens} × ${price}/1M = {cost}",
"aecb952b1e6cce36": "关闭后隐藏 Dock 栏图标,仍可通过菜单栏图标打开应用。",
"aee88743413144a2": "刷新",
"b06325c5660f0c29": "直连",
"b16c3b2ecedd6fe1": "Cursor 接管已生效;添加模型配置后即可使用 BYOK 模型。",
"b4411558b932266f": "上游类型",
"b502b1d414664337": "提示词:{tokens}",
"b5141d3d19e9a048": "是",
"b6725f218ebaef26": "已显示 Dock 栏图标",
"b75a46aad3e7c132": "非缓存输入:{tokens}",
"b79354009c614ae9": "统计数据",
"b86967982067d295": "(估算)",
@@ -303,7 +306,6 @@
"ee239f3943293f87": "周日",
"ee6b89a6a740a4c4": "端口被占用时会自动选择新的随机端口并保存。修改后需要重启软件才会生效。",
"f2bdc88464c51c2e": "显示 API Key",
"f3a76d896853c1df": "未命中",
"f4694c46b1e19602": "最终请求类型",
"f4dcb6a3ceb32247": "第 {page} / {count} 页",
"f4fa9f31ea2ae58d": "过去一年的 Token 用量日历",
+1 -10
View File
@@ -1,4 +1,5 @@
import { Outlet } from "react-router-dom";
import { desktopPlatform } from "../native/platform";
import styles from "./AppFrame.module.scss";
import { AppHeader } from "./AppHeader";
@@ -17,13 +18,3 @@ export function AppFrame() {
</div>
);
}
export type DesktopPlatform = "macos" | "windows" | "linux";
function desktopPlatform(): DesktopPlatform {
// return "windows";
const agent = navigator.userAgent;
if (/Macintosh|Mac OS X/.test(agent)) return "macos";
if (/Windows/.test(agent)) return "windows";
return "linux";
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import appIcon from "../../src-tauri/icons/32x32.png";
import { currentAppVersion } from "../native/appLifecycle";
import type { DesktopPlatform } from "./AppFrame";
import type { DesktopPlatform } from "../native/platform";
import { WindowControls } from "./WindowControls";
import styles from "./AppHeader.module.scss";
+22 -5
View File
@@ -1,14 +1,19 @@
import { getVersion } from "@tauri-apps/api/app";
import { getVersion, setDockVisibility } from "@tauri-apps/api/app";
import { isTauri } from "@tauri-apps/api/core";
import { disable, enable, isEnabled } from "@tauri-apps/plugin-autostart";
import { relaunch } from "@tauri-apps/plugin-process";
import { check, type Update } from "@tauri-apps/plugin-updater";
import { api } from "../api";
import { api, type DesktopSettings } from "../api";
import { desktopPlatform } from "./platform";
export function hasNativeAppLifecycle(): boolean {
return isTauri();
}
export function hasDockVisibilitySetting(): boolean {
return hasNativeAppLifecycle() && desktopPlatform() === "macos";
}
export async function currentAppVersion(): Promise<string> {
return hasNativeAppLifecycle() ? getVersion() : "dev";
}
@@ -21,12 +26,24 @@ export async function writeAutostart(enabled: boolean): Promise<void> {
await (enabled ? enable() : disable());
}
export async function readSilentStart(): Promise<boolean> {
return (await api.desktopSettings()).silent_start;
export async function readDesktopSettings(): Promise<DesktopSettings> {
return api.desktopSettings();
}
export async function writeSilentStart(silentStart: boolean): Promise<void> {
await api.setDesktopSettings({ silent_start: silentStart });
const settings = await readDesktopSettings();
await api.setDesktopSettings({ ...settings, silent_start: silentStart });
}
export async function writeDockIconVisibility(visible: boolean): Promise<void> {
const settings = await readDesktopSettings();
await setDockVisibility(visible);
try {
await api.setDesktopSettings({ ...settings, show_dock_icon: visible });
} catch (cause) {
await setDockVisibility(settings.show_dock_icon).catch(() => {});
throw cause;
}
}
export async function checkForUpdate(): Promise<Update | null> {
+8
View File
@@ -0,0 +1,8 @@
export type DesktopPlatform = "macos" | "windows" | "linux";
export function desktopPlatform(): DesktopPlatform {
const agent = navigator.userAgent;
if (/Macintosh|Mac OS X/.test(agent)) return "macos";
if (/Windows/.test(agent)) return "windows";
return "linux";
}
+40 -1
View File
@@ -47,10 +47,25 @@ pub struct TabSettings {
pub address: String,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct DesktopSettings {
#[serde(default)]
pub silent_start: bool,
#[serde(default = "default_true")]
pub show_dock_icon: bool,
}
impl Default for DesktopSettings {
fn default() -> Self {
Self {
silent_start: false,
show_dock_icon: true,
}
}
}
fn default_true() -> bool {
true
}
impl TabSettings {
@@ -324,6 +339,30 @@ mod tests {
assert_eq!(store.port_settings().await.unwrap(), settings);
}
#[tokio::test]
async fn desktop_settings_show_the_dock_icon_by_default_and_round_trip() {
let store = Store::connect("sqlite::memory:").await.unwrap();
assert_eq!(
store.desktop_settings().await.unwrap(),
DesktopSettings::default()
);
assert_eq!(
serde_json::from_str::<DesktopSettings>(r#"{"silent_start":true}"#).unwrap(),
DesktopSettings {
silent_start: true,
show_dock_icon: true,
}
);
let settings = DesktopSettings {
silent_start: true,
show_dock_icon: false,
};
store.set_desktop_settings(settings).await.unwrap();
assert_eq!(store.desktop_settings().await.unwrap(), settings);
}
#[tokio::test]
async fn proxy_settings_are_write_only_and_preserve_an_unchanged_password() {
let store = Store::connect("sqlite::memory:").await.unwrap();