feat(desktop): add close behavior prompt with quit-on-close setting

Co-authored-by: cherlin <544026227@qq.com>
This commit is contained in:
cherlinbest-pixel 2026-06-21 00:04:52 +08:00 committed by GitHub
parent f32298743d
commit 28a23de604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 243 additions and 3 deletions

View File

@ -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"
/>
<CloseActionPromptDialog
v-if="isDesktop"
v-model:open="showCloseActionPrompt"
@quit="chooseQuit"
@minimize="chooseMinimize"
/>
<QuickOpenDialog :open="showQuickOpen" @update:open="showQuickOpen = $event" @select="handleQuickOpenSelect" />
</div>
<Teleport to="body">

View File

@ -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<DesktopIconTheme>(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(
<Switch id="show-tray-icon" v-model="editShowTrayIcon" />
</div>
<div v-if="!isWeb" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="quit-on-close">{{ t("settings.quitOnClose") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.quitOnCloseDescription") }}</p>
</div>
<Switch id="quit-on-close" v-model="editQuitOnClose" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="update-notifications-enabled">{{ t("settings.updateNotificationsEnabled") }}</Label>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
const open = defineModel<boolean>("open", { required: true });
const emit = defineEmits<{
quit: [];
minimize: [];
}>();
const { t } = useI18n();
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-[440px]" @interact-outside.prevent @escape-key-down.prevent>
<DialogHeader>
<DialogTitle>{{ t("settings.closeActionPromptTitle") }}</DialogTitle>
<DialogDescription>{{ t("settings.closeActionPromptDescription") }}</DialogDescription>
</DialogHeader>
<DialogFooter class="gap-2 sm:gap-2">
<Button type="button" variant="outline" @click="emit('minimize')">
{{ t("settings.closeActionMinimize") }}
</Button>
<Button type="button" @click="emit('quit')">
{{ t("settings.closeActionQuit") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@ -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,
};
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -2250,6 +2250,12 @@ export default {
iconThemeBlackDescription: "ウィンドウ、トレイ、ロゴに黒のDBXマークを使用。",
showTrayIcon: "トレイ/メニューバーアイコンを表示",
showTrayIconDescription: "無効時はアイコンが表示されませんが、ウィンドウを閉じると従来通りDBXはバックグラウンドに隠れます。",
quitOnClose: "ウィンドウを閉じるときに終了",
quitOnCloseDescription: "有効にすると、閉じるボタンでトレイに隠すのではなく、DBXを完全に終了します。",
closeActionPromptTitle: "ウィンドウを閉じる",
closeActionPromptDescription: "閉じるボタンの動作を選択してください。後から「設定 → 外観」で変更できます。",
closeActionQuit: "DBXを終了",
closeActionMinimize: "トレイに最小化",
updateNotificationsEnabled: "アップデート通知を有効にする",
updateNotificationsEnabledDescription: "無効時、DBXはアプリやドライバーのアップデートを自動確認せず、アップデートバッジも表示しません。手動確認は引き続き利用可能です。",
debugLoggingEnabled: "デバッグログを有効にする",

View File

@ -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",

View File

@ -2316,6 +2316,12 @@ export default {
iconThemeBlackDescription: "窗口、托盘和 Logo 都使用黑色 DBX 标识。",
showTrayIcon: "显示系统托盘/菜单栏图标",
showTrayIconDescription: "关闭后不显示图标,但关闭窗口仍会像之前一样隐藏到后台。",
quitOnClose: "关闭窗口时退出程序",
quitOnCloseDescription: "开启后,点击窗口关闭按钮将彻底退出 DBX而不是隐藏到系统托盘。",
closeActionPromptTitle: "关闭窗口",
closeActionPromptDescription: "请选择点击关闭按钮时的行为。之后可在「设置 → 外观」中修改。",
closeActionQuit: "退出程序",
closeActionMinimize: "最小化到托盘",
updateNotificationsEnabled: "启用更新提醒",
updateNotificationsEnabledDescription: "关闭后DBX 不会自动检查应用和驱动更新,也不会显示更新红点;仍可手动检查更新。",
debugLoggingEnabled: "启用调试日志",

View File

@ -2074,6 +2074,12 @@ export default {
iconThemeBlackDescription: "視窗、系統匣和 Logo 都使用黑色 DBX 標誌。",
showTrayIcon: "顯示系統匣/選單欄圖示",
showTrayIconDescription: "關閉後不顯示圖示,但關閉視窗仍會像之前一樣隱藏到後台。",
quitOnClose: "關閉視窗時退出程式",
quitOnCloseDescription: "開啟後,點擊視窗關閉按鈕將徹底退出 DBX而不是隱藏到系統匣。",
closeActionPromptTitle: "關閉視窗",
closeActionPromptDescription: "請選擇點擊關閉按鈕時的行為。之後可在「設定 → 外觀」中修改。",
closeActionQuit: "退出程式",
closeActionMinimize: "最小化到系統匣",
updateNotificationsEnabled: "啟用更新提醒",
updateNotificationsEnabledDescription: "關閉後DBX 不會自動檢查應用程式和驅動程式更新,也不會顯示更新紅點;仍可手動檢查更新。",
debugLoggingEnabled: "啟用偵錯日誌",

View File

@ -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,

View File

@ -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;

View File

@ -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<DesktopSettings> | 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,

View File

@ -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<String>,
@ -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()),

View File

@ -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::<CloseBehaviorState>() {
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::<CloseBehaviorState>() 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![