diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 8fc06baa99..ddd4678d1d 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -26,6 +26,7 @@ import { useDialogSources } from "@/composables/useDialogSources"; import { useNavigationTargets } from "@/composables/useNavigationTargets"; import { useDataGridActions } from "@/composables/useDataGridActions"; import { useTauriEvents } from "@/composables/useTauriEvents"; +import { useCloseActionPrompt } from "@/composables/useCloseActionPrompt"; import { useVisibilityChange } from "@/composables/useVisibilityChange"; import "@/i18n"; import { translateBackendError } from "@/i18n/backend-errors"; @@ -80,6 +81,7 @@ const QueryHistory = defineAsyncComponent(() => import("@/components/editor/Quer const SqlLibraryPanel = defineAsyncComponent(() => import("@/components/layout/SqlLibraryPanel.vue")); const DriverStorePage = defineAsyncComponent(() => import("@/components/config/DriverStoreDialog.vue")); const UpdateDialog = defineAsyncComponent(() => import("@/components/layout/UpdateDialog.vue")); +const CloseActionPromptDialog = defineAsyncComponent(() => import("@/components/layout/CloseActionPromptDialog.vue")); const LoginPage = defineAsyncComponent(() => import("@/components/auth/LoginPage.vue")); const QuickOpenDialog = defineAsyncComponent(() => import("@/components/quick-open/QuickOpenDialog.vue")); @@ -220,6 +222,7 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openDbFilePath, openConnectionDeepLink, }); +const { showCloseActionPrompt, chooseQuit, chooseMinimize, setupCloseActionPromptListener, cleanupCloseActionPromptListener } = useCloseActionPrompt(); useVisibilityChange(); const appVersion = ref(""); @@ -1310,6 +1313,7 @@ onMounted(async () => { }) .catch(() => {}); setupTauriListeners(); + setupCloseActionPromptListener(); void openPendingSqlFiles(); void openPendingDbFiles(); void openPendingConnectionLinks(); @@ -1318,6 +1322,7 @@ onMounted(async () => { onUnmounted(() => { cleanupTauriListeners(); + cleanupCloseActionPromptListener(); if (updateCheckTimer) { clearInterval(updateCheckTimer); } @@ -1538,6 +1543,12 @@ onUnmounted(() => { @download-and-install="downloadAndInstallUpdate" @restart="restartApp" /> + diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 57cedf1728..d72a8272b1 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -106,6 +106,8 @@ const editWordWrap = ref(settingsStore.editorSettings.wordWrap); const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confirmDangerousSqlExecution); const editAppLayout = ref(settingsStore.editorSettings.appLayout); const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon); +const editQuitOnClose = ref(settingsStore.desktopSettings.quit_on_close); +const desktopCloseBehaviorResetPending = ref(false); const editIconTheme = ref(settingsStore.desktopSettings.icon_theme); const editDebugLoggingEnabled = ref(settingsStore.desktopSettings.debug_logging_enabled); const editSidebarTablePageSize = ref(settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE); @@ -365,6 +367,7 @@ watch( editConfirmDangerousSqlExecution.value = settingsStore.editorSettings.confirmDangerousSqlExecution; editAppLayout.value = settingsStore.editorSettings.appLayout; editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon; + editQuitOnClose.value = settingsStore.desktopSettings.quit_on_close; editIconTheme.value = settingsStore.desktopSettings.icon_theme; editDebugLoggingEnabled.value = settingsStore.desktopSettings.debug_logging_enabled; editSidebarTablePageSize.value = settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE; @@ -421,6 +424,7 @@ function hasChanges(): boolean { editConfirmDangerousSqlExecution.value !== settingsStore.editorSettings.confirmDangerousSqlExecution || editAppLayout.value !== settingsStore.editorSettings.appLayout || editShowTrayIcon.value !== settingsStore.desktopSettings.show_tray_icon || + editQuitOnClose.value !== settingsStore.desktopSettings.quit_on_close || editIconTheme.value !== settingsStore.desktopSettings.icon_theme || editDebugLoggingEnabled.value !== settingsStore.desktopSettings.debug_logging_enabled || editSidebarTablePageSize.value !== (settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE) || @@ -484,10 +488,13 @@ async function persistSettings() { }); await settingsStore.updateDesktopSettings({ show_tray_icon: editShowTrayIcon.value, + quit_on_close: editQuitOnClose.value, + close_action_prompted: desktopCloseBehaviorResetPending.value ? false : true, icon_theme: editIconTheme.value, debug_logging_enabled: editDebugLoggingEnabled.value, sidebar_table_page_size: editSidebarTablePageSize.value, }); + desktopCloseBehaviorResetPending.value = false; if (sidebarObjectDisplayChanged) { await connectionStore.refreshAllTree(); } @@ -514,6 +521,8 @@ function resetDefaults() { editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution; editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon; + editQuitOnClose.value = DEFAULT_DESKTOP_SETTINGS.quit_on_close; + desktopCloseBehaviorResetPending.value = true; editIconTheme.value = DEFAULT_DESKTOP_SETTINGS.icon_theme; editDebugLoggingEnabled.value = DEFAULT_DESKTOP_SETTINGS.debug_logging_enabled; editSidebarTablePageSize.value = DEFAULT_SIDEBAR_TABLE_PAGE_SIZE; @@ -1057,6 +1066,7 @@ watch( await settingsStore.initAiConfig(); await settingsStore.initDesktopSettings(); editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon; + editQuitOnClose.value = settingsStore.desktopSettings.quit_on_close; editIconTheme.value = settingsStore.desktopSettings.icon_theme; editDebugLoggingEnabled.value = settingsStore.desktopSettings.debug_logging_enabled; editSidebarTablePageSize.value = settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE; @@ -1872,6 +1882,14 @@ watch( + + + {{ t("settings.quitOnClose") }} + {{ t("settings.quitOnCloseDescription") }} + + + + {{ t("settings.updateNotificationsEnabled") }} diff --git a/apps/desktop/src/components/layout/CloseActionPromptDialog.vue b/apps/desktop/src/components/layout/CloseActionPromptDialog.vue new file mode 100644 index 0000000000..958c46a44a --- /dev/null +++ b/apps/desktop/src/components/layout/CloseActionPromptDialog.vue @@ -0,0 +1,33 @@ + + + + + + + {{ t("settings.closeActionPromptTitle") }} + {{ t("settings.closeActionPromptDescription") }} + + + + {{ t("settings.closeActionMinimize") }} + + + {{ t("settings.closeActionQuit") }} + + + + + diff --git a/apps/desktop/src/composables/useCloseActionPrompt.ts b/apps/desktop/src/composables/useCloseActionPrompt.ts new file mode 100644 index 0000000000..5d3275ea78 --- /dev/null +++ b/apps/desktop/src/composables/useCloseActionPrompt.ts @@ -0,0 +1,55 @@ +import { ref } from "vue"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { isTauriRuntime } from "@/lib/tauriRuntime"; + +export function useCloseActionPrompt() { + const settingsStore = useSettingsStore(); + const showCloseActionPrompt = ref(false); + const unlistenHandles: Array<() => void> = []; + + async function applyCloseChoice(quitOnClose: boolean) { + showCloseActionPrompt.value = false; + await settingsStore.updateDesktopSettings({ + quit_on_close: quitOnClose, + close_action_prompted: true, + }); + if (!isTauriRuntime()) return; + if (quitOnClose) { + const { exit } = await import("@tauri-apps/plugin-process"); + await exit(0); + return; + } + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + await getCurrentWindow().hide(); + } + + function chooseQuit() { + void applyCloseChoice(true); + } + + function chooseMinimize() { + void applyCloseChoice(false); + } + + function setupCloseActionPromptListener() { + if (!isTauriRuntime()) return; + void import("@tauri-apps/api/event").then(({ listen }) => { + listen("dbx-close-action-prompt", () => { + showCloseActionPrompt.value = true; + }).then((unlisten) => unlistenHandles.push(unlisten)); + }); + } + + function cleanupCloseActionPromptListener() { + unlistenHandles.forEach((unlisten) => unlisten()); + unlistenHandles.length = 0; + } + + return { + showCloseActionPrompt, + chooseQuit, + chooseMinimize, + setupCloseActionPromptListener, + cleanupCloseActionPromptListener, + }; +} diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 94312e984e..b6b99dd033 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2292,6 +2292,12 @@ export default { iconThemeBlackDescription: "Use the black DBX mark for the window, tray, and logo.", showTrayIcon: "Show tray/menu bar icon", showTrayIconDescription: "When disabled, no icon is shown, but closing the window still hides DBX in the background as before.", + quitOnClose: "Quit when closing window", + quitOnCloseDescription: "When enabled, clicking the close button exits DBX completely instead of hiding it to the tray. You can change this later in Appearance settings.", + closeActionPromptTitle: "Close window", + closeActionPromptDescription: "Choose what happens when you click the close button. You can change this later in Settings > Appearance.", + closeActionQuit: "Quit DBX", + closeActionMinimize: "Minimize to tray", updateNotificationsEnabled: "Enable update reminders", updateNotificationsEnabledDescription: "When disabled, DBX will not automatically check app or driver updates or show update badges. Manual checks are still available.", debugLoggingEnabled: "Enable debug logs", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index ee5586683a..58fef329fc 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1954,6 +1954,12 @@ export default { iconThemeBlackDescription: "Usar la marca negra de DBX en ventana, bandeja y logo.", showTrayIcon: "Mostrar icono en bandeja/barra de menú", showTrayIconDescription: "Si está desactivado, no se muestra el icono, pero cerrar la ventana sigue ocultando DBX en segundo plano como antes.", + quitOnClose: "Salir al cerrar la ventana", + quitOnCloseDescription: "Si está activado, al pulsar el botón de cerrar DBX se cierra por completo en lugar de ocultarse en la bandeja.", + closeActionPromptTitle: "Cerrar ventana", + closeActionPromptDescription: "Elige qué ocurre al pulsar el botón de cerrar. Puedes cambiarlo después en Ajustes → Apariencia.", + closeActionQuit: "Salir de DBX", + closeActionMinimize: "Minimizar a la bandeja", updateNotificationsEnabled: "Activar recordatorios de actualización", updateNotificationsEnabledDescription: "Al desactivarlo, DBX no comprobará automáticamente actualizaciones de la app ni de los controladores, ni mostrará indicadores. Las comprobaciones manuales siguen disponibles.", debugLoggingEnabled: "Activar logs de depuración", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 1cbb0efa94..bb409457c0 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2027,6 +2027,12 @@ export default { iconThemeBlackDescription: "Utilizza il marchio DBX nero per la finestra, la barra delle applicazioni e il logo.", showTrayIcon: "Mostra icona nella barra delle applicazioni/menu", showTrayIconDescription: "Se disattivato, non viene mostrata alcuna icona, ma la chiusura della finestra nasconderà comunque DBX in background.", + quitOnClose: "Esci alla chiusura della finestra", + quitOnCloseDescription: "Se attivato, il pulsante di chiusura termina completamente DBX invece di nasconderlo nella barra delle applicazioni.", + closeActionPromptTitle: "Chiudi finestra", + closeActionPromptDescription: "Scegli cosa succede quando clicchi il pulsante di chiusura. Puoi modificarlo in seguito in Impostazioni → Aspetto.", + closeActionQuit: "Esci da DBX", + closeActionMinimize: "Riduci a icona", updateNotificationsEnabled: "Abilita promemoria aggiornamenti", updateNotificationsEnabledDescription: "Se disattivato, DBX non controllerà automaticamente gli aggiornamenti dell'app o dei driver e non mostrerà badge. I controlli manuali restano disponibili.", debugLoggingEnabled: "Abilita log di debug", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index b2a863f9b9..00584526b1 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2250,6 +2250,12 @@ export default { iconThemeBlackDescription: "ウィンドウ、トレイ、ロゴに黒のDBXマークを使用。", showTrayIcon: "トレイ/メニューバーアイコンを表示", showTrayIconDescription: "無効時はアイコンが表示されませんが、ウィンドウを閉じると従来通りDBXはバックグラウンドに隠れます。", + quitOnClose: "ウィンドウを閉じるときに終了", + quitOnCloseDescription: "有効にすると、閉じるボタンでトレイに隠すのではなく、DBXを完全に終了します。", + closeActionPromptTitle: "ウィンドウを閉じる", + closeActionPromptDescription: "閉じるボタンの動作を選択してください。後から「設定 → 外観」で変更できます。", + closeActionQuit: "DBXを終了", + closeActionMinimize: "トレイに最小化", updateNotificationsEnabled: "アップデート通知を有効にする", updateNotificationsEnabledDescription: "無効時、DBXはアプリやドライバーのアップデートを自動確認せず、アップデートバッジも表示しません。手動確認は引き続き利用可能です。", debugLoggingEnabled: "デバッグログを有効にする", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index e19c637c33..ba46102a46 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2038,6 +2038,12 @@ export default { iconThemeBlackDescription: "Usar a marca DBX em preto na janela, na bandeja e no logo.", showTrayIcon: "Mostrar ícone na bandeja/barra de menus", showTrayIconDescription: "Quando desativado, nenhum ícone é exibido, mas fechar a janela ainda oculta o DBX em segundo plano como antes.", + quitOnClose: "Sair ao fechar a janela", + quitOnCloseDescription: "Quando ativado, o botão de fechar encerra o DBX completamente em vez de minimizar para a bandeja.", + closeActionPromptTitle: "Fechar janela", + closeActionPromptDescription: "Escolha o que acontece ao clicar no botão de fechar. Você pode alterar depois em Configurações → Aparência.", + closeActionQuit: "Sair do DBX", + closeActionMinimize: "Minimizar para a bandeja", updateNotificationsEnabled: "Ativar lembretes de atualização", updateNotificationsEnabledDescription: "Quando desativado, o DBX não verificará automaticamente atualizações do app ou dos drivers nem mostrará indicadores. Verificações manuais continuam disponíveis.", debugLoggingEnabled: "Ativar logs de depuração", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 7152e0e66a..267a4adcaa 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2316,6 +2316,12 @@ export default { iconThemeBlackDescription: "窗口、托盘和 Logo 都使用黑色 DBX 标识。", showTrayIcon: "显示系统托盘/菜单栏图标", showTrayIconDescription: "关闭后不显示图标,但关闭窗口仍会像之前一样隐藏到后台。", + quitOnClose: "关闭窗口时退出程序", + quitOnCloseDescription: "开启后,点击窗口关闭按钮将彻底退出 DBX,而不是隐藏到系统托盘。", + closeActionPromptTitle: "关闭窗口", + closeActionPromptDescription: "请选择点击关闭按钮时的行为。之后可在「设置 → 外观」中修改。", + closeActionQuit: "退出程序", + closeActionMinimize: "最小化到托盘", updateNotificationsEnabled: "启用更新提醒", updateNotificationsEnabledDescription: "关闭后,DBX 不会自动检查应用和驱动更新,也不会显示更新红点;仍可手动检查更新。", debugLoggingEnabled: "启用调试日志", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index dc7a482aa9..2b32e7da2e 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2074,6 +2074,12 @@ export default { iconThemeBlackDescription: "視窗、系統匣和 Logo 都使用黑色 DBX 標誌。", showTrayIcon: "顯示系統匣/選單欄圖示", showTrayIconDescription: "關閉後不顯示圖示,但關閉視窗仍會像之前一樣隱藏到後台。", + quitOnClose: "關閉視窗時退出程式", + quitOnCloseDescription: "開啟後,點擊視窗關閉按鈕將徹底退出 DBX,而不是隱藏到系統匣。", + closeActionPromptTitle: "關閉視窗", + closeActionPromptDescription: "請選擇點擊關閉按鈕時的行為。之後可在「設定 → 外觀」中修改。", + closeActionQuit: "退出程式", + closeActionMinimize: "最小化到系統匣", updateNotificationsEnabled: "啟用更新提醒", updateNotificationsEnabledDescription: "關閉後,DBX 不會自動檢查應用程式和驅動程式更新,也不會顯示更新紅點;仍可手動檢查更新。", debugLoggingEnabled: "啟用偵錯日誌", diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 42067f84db..bc1e0f2ed5 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -1,4 +1,4 @@ -import type { +import type { ConnectionConfig, DatabaseInfo, LinkedServerInfo, @@ -104,6 +104,8 @@ const DESKTOP_SETTINGS_STORAGE_KEY = "dbx-desktop-settings"; const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { show_tray_icon: true, icon_theme: "default", + quit_on_close: false, + close_action_prompted: false, debug_logging_enabled: false, saved_sql_sync_dir: null, driver_store_dir: null, diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 5e861b767d..199ebe0f50 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import type { ConnectionConfig, @@ -126,6 +126,8 @@ export interface DriverRuntimeSummary { export interface DesktopSettings { show_tray_icon: boolean; icon_theme: "default" | "black"; + quit_on_close: boolean; + close_action_prompted: boolean; debug_logging_enabled: boolean; saved_sql_sync_dir?: string | null; driver_store_dir?: string | null; diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 38b5a83f23..28cca0839b 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -42,6 +42,8 @@ export interface AiTestConnectionResult { export interface DesktopSettings { show_tray_icon: boolean; icon_theme: DesktopIconTheme; + quit_on_close: boolean; + close_action_prompted: boolean; debug_logging_enabled: boolean; saved_sql_sync_dir?: string | null; driver_store_dir?: string | null; @@ -59,6 +61,8 @@ export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000; export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { show_tray_icon: true, icon_theme: "default", + quit_on_close: false, + close_action_prompted: false, debug_logging_enabled: false, saved_sql_sync_dir: null, driver_store_dir: null, @@ -73,6 +77,8 @@ function normalizeDesktopSettings(settings: Partial | null | un return { show_tray_icon: settings?.show_tray_icon ?? DEFAULT_DESKTOP_SETTINGS.show_tray_icon, icon_theme: iconTheme, + quit_on_close: settings?.quit_on_close ?? DEFAULT_DESKTOP_SETTINGS.quit_on_close, + close_action_prompted: settings?.close_action_prompted ?? DEFAULT_DESKTOP_SETTINGS.close_action_prompted, debug_logging_enabled: settings?.debug_logging_enabled ?? DEFAULT_DESKTOP_SETTINGS.debug_logging_enabled, saved_sql_sync_dir: settings?.saved_sql_sync_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.saved_sql_sync_dir, driver_store_dir: settings?.driver_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.driver_store_dir, diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index 5987f22fb0..12dc41a2dc 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -37,6 +37,10 @@ pub struct DesktopSettings { pub show_tray_icon: bool, pub icon_theme: DesktopIconTheme, #[serde(default)] + pub quit_on_close: bool, + #[serde(default)] + pub close_action_prompted: bool, + #[serde(default)] pub debug_logging_enabled: bool, #[serde(default)] pub saved_sql_sync_dir: Option, @@ -59,6 +63,8 @@ impl Default for DesktopSettings { Self { show_tray_icon: true, icon_theme: DesktopIconTheme::Default, + quit_on_close: false, + close_action_prompted: false, debug_logging_enabled: false, saved_sql_sync_dir: None, driver_store_dir: None, @@ -557,6 +563,11 @@ impl Storage { "icon_theme".to_string(), serde_json::to_value(desktop_settings.icon_theme).map_err(|e| e.to_string())?, ); + settings.insert("quit_on_close".to_string(), serde_json::Value::Bool(desktop_settings.quit_on_close)); + settings.insert( + "close_action_prompted".to_string(), + serde_json::Value::Bool(desktop_settings.close_action_prompted), + ); settings.insert( "debug_logging_enabled".to_string(), serde_json::Value::Bool(desktop_settings.debug_logging_enabled), @@ -609,6 +620,14 @@ impl Storage { .or_else(|| settings.get("run_in_background").and_then(|value| value.as_bool())) .unwrap_or_else(|| DesktopSettings::default().show_tray_icon), icon_theme: DesktopIconTheme::from_settings_value(settings.get("icon_theme")), + quit_on_close: settings + .get("quit_on_close") + .and_then(|value| value.as_bool()) + .unwrap_or_else(|| DesktopSettings::default().quit_on_close), + close_action_prompted: settings + .get("close_action_prompted") + .and_then(|value| value.as_bool()) + .unwrap_or_else(|| DesktopSettings::default().close_action_prompted), debug_logging_enabled: settings .get("debug_logging_enabled") .and_then(|value| value.as_bool()) @@ -2084,6 +2103,8 @@ mod tests { .save_desktop_settings(&DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black, + quit_on_close: true, + close_action_prompted: false, debug_logging_enabled: true, saved_sql_sync_dir: None, driver_store_dir: Some("/tmp/dbx-drivers".to_string()), @@ -2100,6 +2121,8 @@ mod tests { DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black, + quit_on_close: true, + close_action_prompted: false, debug_logging_enabled: true, saved_sql_sync_dir: None, driver_store_dir: Some("/tmp/dbx-drivers".to_string()), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3fb547196e..b0057b8042 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ mod window_state_guard; use commands::connection::AppState; use dbx_core::storage::{DesktopIconTheme, DesktopSettings, Storage}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; use tauri::RunEvent; @@ -18,6 +19,33 @@ use tauri::{Emitter, Manager}; use tauri_plugin_deep_link::DeepLinkExt; const DESKTOP_TRAY_ID: &str = "main-tray"; + +pub struct CloseBehaviorState { + quit_on_close: AtomicBool, + prompted: AtomicBool, +} + +impl CloseBehaviorState { + fn new(settings: &DesktopSettings) -> Self { + Self { + quit_on_close: AtomicBool::new(settings.quit_on_close), + prompted: AtomicBool::new(settings.close_action_prompted), + } + } + + fn apply(&self, settings: &DesktopSettings) { + self.quit_on_close.store(settings.quit_on_close, Ordering::Relaxed); + self.prompted.store(settings.close_action_prompted, Ordering::Relaxed); + } + + fn quit_on_close(&self) -> bool { + self.quit_on_close.load(Ordering::Relaxed) + } + + fn prompted(&self) -> bool { + self.prompted.load(Ordering::Relaxed) + } +} #[cfg(target_os = "macos")] const MACOS_TRAY_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/tray-macos-template.png"); const BLACK_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon-black.png"); @@ -171,6 +199,9 @@ fn apply_desktop_tray_icon_theme(app: &tauri::AppHandle, icon_theme: DesktopIcon pub(crate) fn apply_desktop_settings(app: &tauri::AppHandle, desktop_settings: &DesktopSettings) -> tauri::Result<()> { apply_debug_log_level(desktop_settings.debug_logging_enabled); + if let Some(state) = app.try_state::() { + state.apply(desktop_settings); + } apply_desktop_icon_theme(app, desktop_settings.icon_theme)?; if matches!(std::env::consts::OS, "macos" | "windows") { if let Some(tray) = app.tray_by_id(DESKTOP_TRAY_ID) { @@ -309,6 +340,7 @@ pub fn run() { app.manage(commands::external_sql::ExternalSqlOpenState::default()); app.manage(commands::external_db::ExternalDbOpenState::default()); app.manage(commands::deep_link::DeepLinkOpenState::default()); + app.manage(CloseBehaviorState::new(&desktop_settings)); let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1)); open_connection_deep_links(app.handle(), startup_links); @@ -336,10 +368,26 @@ pub fn run() { }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { - if should_hide_window_on_close(std::env::consts::OS) { + if !should_hide_window_on_close(std::env::consts::OS) { + return; + } + let app = window.app_handle(); + let Some(state) = app.try_state::() else { let _ = window.hide(); api.prevent_close(); + return; + }; + if !state.prompted() { + api.prevent_close(); + let _ = app.emit("dbx-close-action-prompt", ()); + return; } + if state.quit_on_close() { + app.exit(0); + return; + } + let _ = window.hide(); + api.prevent_close(); } }) .invoke_handler(tauri::generate_handler![
{{ t("settings.quitOnCloseDescription") }}