From bc41936e4882c96b6b7e7bda4f97c5d6c0987886 Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Fri, 22 May 2026 02:24:16 +0800
Subject: [PATCH] feat(app): configure background tray behavior
---
.../editor/EditorSettingsDialog.vue | 22 +++-
apps/desktop/src/i18n/locales/en.ts | 3 +
apps/desktop/src/i18n/locales/es.ts | 3 +
apps/desktop/src/i18n/locales/zh-CN.ts | 3 +
apps/desktop/src/lib/api.ts | 2 +
apps/desktop/src/lib/http.ts | 9 ++
apps/desktop/src/lib/tauri.ts | 12 ++
apps/desktop/src/stores/settingsStore.ts | 34 ++++++
crates/dbx-core/src/storage.rs | 111 ++++++++++++++++--
src-tauri/src/commands/app_settings.rs | 25 ++++
src-tauri/src/commands/mod.rs | 1 +
src-tauri/src/lib.rs | 92 +++++++++++----
12 files changed, 282 insertions(+), 35 deletions(-)
create mode 100644 src-tauri/src/commands/app_settings.rs
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index cfb37e3493..c717586a55 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -18,6 +18,7 @@ import {
EDITOR_THEMES,
FONT_FAMILIES,
DEFAULT_EDITOR_SETTINGS,
+ DEFAULT_DESKTOP_SETTINGS,
type AiProvider,
type AiApiStyle,
type EditorTheme,
@@ -59,6 +60,7 @@ const editTheme = ref(settingsStore.editorSettings.theme);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
+const editRunInBackground = ref(settingsStore.desktopSettings.run_in_background);
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation);
@@ -122,6 +124,7 @@ watch(
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editWordWrap.value = settingsStore.editorSettings.wordWrap;
editAppLayout.value = settingsStore.editorSettings.appLayout;
+ editRunInBackground.value = settingsStore.desktopSettings.run_in_background;
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation;
@@ -152,6 +155,7 @@ function hasChanges(): boolean {
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
+ editRunInBackground.value !== settingsStore.desktopSettings.run_in_background ||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation ||
@@ -161,7 +165,7 @@ function hasChanges(): boolean {
);
}
-function applySettings() {
+async function applySettings() {
if (hasBlockingShortcutConflicts.value) return;
settingsStore.updateEditorSettings({
fontFamily: editFontFamily.value,
@@ -176,6 +180,9 @@ function applySettings() {
autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
});
+ await settingsStore.updateDesktopSettings({
+ run_in_background: editRunInBackground.value,
+ });
emit("update:open", false);
}
@@ -186,6 +193,7 @@ function resetDefaults() {
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
+ editRunInBackground.value = DEFAULT_DESKTOP_SETTINGS.run_in_background;
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation;
@@ -292,6 +300,8 @@ watch(
newPassword.value = "";
confirmNewPassword.value = "";
await settingsStore.initAiConfig();
+ await settingsStore.initDesktopSettings();
+ editRunInBackground.value = settingsStore.desktopSettings.run_in_background;
syncAiEditState();
}
},
@@ -825,6 +835,16 @@ watch(
+
+
+
+
{{ t("settings.runInBackgroundDescription") }}
+
+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 4a05db5c9d..5fb636e57f 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1194,6 +1194,9 @@ export default {
appLayoutSeparatedDescription: "Clear panel separation with block-style tabs.",
appLayoutClassic: "Classic compact",
appLayoutClassicDescription: "Continuous tab bar with compact panel headers.",
+ runInBackground: "Keep running after closing window",
+ runInBackgroundDescription:
+ "When enabled, DBX shows a tray/menu bar icon and closing the window keeps it in the background. When disabled, no icon is shown and closing the window quits the app.",
sidebarActivation: "Sidebar activation",
sidebarActivationSingle: "Single click",
sidebarActivationSingleDescription: "Open actionable sidebar items with one click.",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index a1db082bda..b2aab95c70 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -1092,6 +1092,9 @@ export default {
appLayoutSeparatedDescription: "Separación clara de paneles con pestañas en bloque.",
appLayoutClassic: "Clásico compacto",
appLayoutClassicDescription: "Barra de pestañas continua con encabezados de panel compactos.",
+ runInBackground: "Seguir ejecutando al cerrar la ventana",
+ runInBackgroundDescription:
+ "Si está activado, DBX muestra un icono en la bandeja/barra de menú y cerrar la ventana lo deja en segundo plano. Si está desactivado, no se muestra el icono y cerrar la ventana sale de la app.",
sidebarActivation: "Activación de la barra lateral",
sidebarActivationSingle: "Un clic",
sidebarActivationSingleDescription: "Abrir elementos accionables de la barra lateral con un clic.",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 6dda0a7932..5051d719a1 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1172,6 +1172,9 @@ export default {
appLayoutSeparatedDescription: "面板分隔更清晰,标签页为块状样式。",
appLayoutClassic: "经典紧凑",
appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。",
+ runInBackground: "关闭窗口后后台驻留",
+ runInBackgroundDescription:
+ "开启时显示系统托盘/菜单栏图标,关闭窗口会隐藏到后台;关闭后不显示图标,关闭窗口将退出应用。",
sidebarActivation: "侧边栏打开方式",
sidebarActivationSingle: "单击打开",
sidebarActivationSingleDescription: "单击即可打开侧边栏中的可操作项目。",
diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts
index fac2242ab4..6fcbd68527 100644
--- a/apps/desktop/src/lib/api.ts
+++ b/apps/desktop/src/lib/api.ts
@@ -95,6 +95,8 @@ export const aiTestConnection = forward("aiTestConnection");
export const aiListModels = forward("aiListModels");
export const saveAiConfig = forward("saveAiConfig");
export const loadAiConfig = forward("loadAiConfig");
+export const loadDesktopSettings = forward("loadDesktopSettings");
+export const saveDesktopSettings = forward("saveDesktopSettings");
export const saveAiConversation = forward("saveAiConversation");
export const loadAiConversations = forward("loadAiConversations");
export const deleteAiConversation = forward("deleteAiConversation");
diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts
index 6068c394cc..8a4c0ea8e5 100644
--- a/apps/desktop/src/lib/http.ts
+++ b/apps/desktop/src/lib/http.ts
@@ -25,6 +25,7 @@ import type {
AiStreamChunk,
AiConversation,
AiModelInfo,
+ DesktopSettings,
DriverInstallProgress,
JavaRuntimeConfig,
UpdateInfo,
@@ -466,6 +467,14 @@ export async function loadAiConfig(): Promise {
return get("/api/ai/config");
}
+export async function loadDesktopSettings(): Promise {
+ return { run_in_background: true };
+}
+
+export async function saveDesktopSettings(_settings: DesktopSettings): Promise {
+ return;
+}
+
// --- AI Conversations ---
export async function saveAiConversation(conversation: AiConversation): Promise {
diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts
index 8022f4fb3d..faa56cd1d4 100644
--- a/apps/desktop/src/lib/tauri.ts
+++ b/apps/desktop/src/lib/tauri.ts
@@ -40,6 +40,10 @@ export interface JavaRuntimeConfig {
custom_java_path: string | null;
}
+export interface DesktopSettings {
+ run_in_background: boolean;
+}
+
export interface DriverInstallProgress {
step: string;
downloaded?: number;
@@ -117,6 +121,14 @@ export async function loadAiConfig(): Promise {
return invoke("load_ai_config");
}
+export async function loadDesktopSettings(): Promise {
+ return invoke("load_desktop_settings");
+}
+
+export async function saveDesktopSettings(settings: DesktopSettings): Promise {
+ return invoke("save_desktop_settings", { settings });
+}
+
export async function listSystemFonts(): Promise {
return invoke("list_system_fonts");
}
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 7f4697657b..93679c2748 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -34,6 +34,14 @@ export interface AiConfig {
enableThinking?: boolean;
}
+export interface DesktopSettings {
+ run_in_background: boolean;
+}
+
+export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
+ run_in_background: true,
+};
+
export interface AiProviderPreset extends Omit {
label: string;
iconSlug?: string;
@@ -311,9 +319,32 @@ function saveEditorSettings(settings: EditorSettings) {
export const useSettingsStore = defineStore("settings", () => {
const aiConfig = ref(normalizeAiConfig({ provider: "claude" }));
const isAiConfigLoaded = ref(false);
+ const desktopSettings = ref({ ...DEFAULT_DESKTOP_SETTINGS });
+ const isDesktopSettingsLoaded = ref(false);
const editorSettings = ref(loadEditorSettings());
+ async function initDesktopSettings() {
+ if (isDesktopSettingsLoaded.value) return;
+ desktopSettings.value = await api.loadDesktopSettings().catch(() => ({ ...DEFAULT_DESKTOP_SETTINGS }));
+ isDesktopSettingsLoaded.value = true;
+ }
+
+ async function updateDesktopSettings(partial: Partial) {
+ const previous = desktopSettings.value;
+ const next = {
+ ...desktopSettings.value,
+ ...partial,
+ };
+ desktopSettings.value = next;
+ try {
+ await api.saveDesktopSettings(next);
+ } catch (error) {
+ desktopSettings.value = previous;
+ throw error;
+ }
+ }
+
async function initAiConfig() {
if (isAiConfigLoaded.value) return;
const legacy = localStorage.getItem("dbx-ai-config");
@@ -397,7 +428,10 @@ export const useSettingsStore = defineStore("settings", () => {
updateAiConfig,
isConfigured,
editorSettings,
+ desktopSettings,
updateEditorSettings,
+ initDesktopSettings,
+ updateDesktopSettings,
updateColumnFormatter,
upsertCustomColumnFormatter,
deleteCustomColumnFormatter,
diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs
index 98ee04464e..303e5854e6 100644
--- a/crates/dbx-core/src/storage.rs
+++ b/crates/dbx-core/src/storage.rs
@@ -1,6 +1,7 @@
use std::path::Path;
use std::str::FromStr;
+use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use crate::ai::{AiChatMessage, AiConfig, AiConversation};
@@ -12,6 +13,17 @@ pub struct Storage {
db: SqlitePool,
}
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct DesktopSettings {
+ pub run_in_background: bool,
+}
+
+impl Default for DesktopSettings {
+ fn default() -> Self {
+ Self { run_in_background: true }
+ }
+}
+
const SCHEMA_STATEMENTS: &[&str] = &[
"CREATE TABLE IF NOT EXISTS connections (
id TEXT PRIMARY KEY,
@@ -274,8 +286,25 @@ impl Storage {
// ---------------------------------------------------------------------------
impl Storage {
- pub async fn save_password_hash(&self, hash: &str) -> Result<(), String> {
- let json = serde_json::json!({ "password_hash": hash }).to_string();
+ async fn load_app_settings_json(&self) -> Result, String> {
+ let row: Option<(String,)> = sqlx::query_as("SELECT settings_json FROM app_settings WHERE id = 1")
+ .fetch_optional(&self.db)
+ .await
+ .map_err(|e| e.to_string())?;
+ let Some((json,)) = row else {
+ return Ok(serde_json::Map::new());
+ };
+ match serde_json::from_str::(&json).map_err(|e| e.to_string())? {
+ serde_json::Value::Object(map) => Ok(map),
+ _ => Ok(serde_json::Map::new()),
+ }
+ }
+
+ async fn save_app_settings_json(
+ &self,
+ settings: &serde_json::Map,
+ ) -> Result<(), String> {
+ let json = serde_json::Value::Object(settings.clone()).to_string();
sqlx::query("INSERT OR REPLACE INTO app_settings (id, settings_json) VALUES (1, ?)")
.bind(&json)
.execute(&self.db)
@@ -284,18 +313,31 @@ impl Storage {
Ok(())
}
+ pub async fn save_password_hash(&self, hash: &str) -> Result<(), String> {
+ let mut settings = self.load_app_settings_json().await?;
+ settings.insert("password_hash".to_string(), serde_json::Value::String(hash.to_string()));
+ self.save_app_settings_json(&settings).await
+ }
+
pub async fn load_password_hash(&self) -> Result