mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-30 11:59:29 +02:00
Merge pull request #391 from huy97/feature/close-confirm-tray-dialog
feat: confirm minimize-to-tray or quit when closing the window
This commit is contained in:
Generated
+1
@@ -6606,6 +6606,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"image",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
|
||||
@@ -35,7 +35,7 @@ resvg = "0.47"
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2", features = ["devtools", "test"] }
|
||||
tauri = { version = "2", features = ["devtools", "test", "tray-icon", "image-png"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
+113
-1
@@ -1,13 +1,19 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
use std::env;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||
use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_log::{Target, TargetKind};
|
||||
|
||||
// Store pending URLs that need to be handled when the window is ready
|
||||
static PENDING_URLS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
|
||||
// Set to true once the user has confirmed they want to quit, so the close
|
||||
// interceptor lets the next CloseRequested through instead of looping back
|
||||
// to the confirmation dialog.
|
||||
static QUIT_CONFIRMED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
mod api_client;
|
||||
mod api_server;
|
||||
mod app_auto_updater;
|
||||
@@ -1145,6 +1151,30 @@ async fn generate_sample_fingerprint(
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirm a quit chosen from the close-confirmation dialog and exit the app.
|
||||
#[tauri::command]
|
||||
fn confirm_quit(app_handle: tauri::AppHandle) {
|
||||
QUIT_CONFIRMED.store(true, Ordering::SeqCst);
|
||||
app_handle.exit(0);
|
||||
}
|
||||
|
||||
/// Hide the main window so the app keeps running behind its tray icon.
|
||||
#[tauri::command]
|
||||
fn hide_to_tray(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
window.hide().map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_main_window(app_handle: &tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
@@ -1243,6 +1273,86 @@ pub fn run() {
|
||||
#[allow(unused_variables)]
|
||||
let window = win_builder.build().unwrap();
|
||||
|
||||
// System tray so the user can keep the app running after the close
|
||||
// dialog's "Minimize" action hides the window.
|
||||
{
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
let show_item = MenuItemBuilder::with_id("tray_show", "Show Donut Browser")
|
||||
.build(app)
|
||||
.map_err(|e| format!("Failed to build tray show item: {e}"))?;
|
||||
let quit_item = MenuItemBuilder::with_id("tray_quit", "Quit")
|
||||
.build(app)
|
||||
.map_err(|e| format!("Failed to build tray quit item: {e}"))?;
|
||||
let tray_menu = MenuBuilder::new(app)
|
||||
.item(&show_item)
|
||||
.separator()
|
||||
.item(&quit_item)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build tray menu: {e}"))?;
|
||||
|
||||
// Tray-specific icons. macOS/Linux get a template (black + alpha)
|
||||
// version so the OS can tint it for light/dark menu bars; Windows
|
||||
// gets the full-color variant. Decode through the `image` crate so
|
||||
// we hand Tauri raw RGBA — `Image::from_bytes` can fail silently on
|
||||
// bitmaps that don't match the size Tauri expects.
|
||||
#[cfg(target_os = "windows")]
|
||||
let tray_icon_bytes: &[u8] = include_bytes!("../icons/tray-icon-win-44.png");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let tray_icon_bytes: &[u8] = include_bytes!("../icons/tray-icon-44.png");
|
||||
let tray_rgba = image::load_from_memory(tray_icon_bytes)
|
||||
.map_err(|e| format!("Failed to decode tray icon: {e}"))?
|
||||
.into_rgba8();
|
||||
let (tray_w, tray_h) = tray_rgba.dimensions();
|
||||
let tray_image = tauri::image::Image::new_owned(tray_rgba.into_raw(), tray_w, tray_h);
|
||||
|
||||
let _tray = TrayIconBuilder::with_id("main")
|
||||
.icon(tray_image)
|
||||
.icon_as_template(cfg!(not(target_os = "windows")))
|
||||
.tooltip("Donut Browser")
|
||||
.menu(&tray_menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(|app_handle, event| match event.id().as_ref() {
|
||||
"tray_show" => show_main_window(app_handle),
|
||||
"tray_quit" => {
|
||||
QUIT_CONFIRMED.store(true, Ordering::SeqCst);
|
||||
app_handle.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
show_main_window(tray.app_handle());
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.map_err(|e| format!("Failed to build tray icon: {e}"))?;
|
||||
}
|
||||
|
||||
// Intercept the window close so the frontend can ask the user whether
|
||||
// to minimize or quit. The app exits when `confirm_quit` flips
|
||||
// QUIT_CONFIRMED — until then, every CloseRequested is held back.
|
||||
{
|
||||
let app_handle = app.handle().clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
if QUIT_CONFIRMED.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
api.prevent_close();
|
||||
if let Err(e) = app_handle.emit("close-confirm-requested", ()) {
|
||||
log::warn!("Failed to emit close-confirm-requested: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set transparent titlebar for macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -1954,6 +2064,8 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
confirm_quit,
|
||||
hide_to_tray,
|
||||
get_supported_browsers,
|
||||
is_browser_supported_on_platform,
|
||||
download_browser,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { AccountPage } from "@/components/account-page";
|
||||
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
|
||||
import { CloneProfileDialog } from "@/components/clone-profile-dialog";
|
||||
import { CloseConfirmDialog } from "@/components/close-confirm-dialog";
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { CommercialTrialModal } from "@/components/commercial-trial-modal";
|
||||
import { CookieCopyDialog } from "@/components/cookie-copy-dialog";
|
||||
@@ -1431,6 +1432,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-background font-(family-name:--font-geist-sans)">
|
||||
<CloseConfirmDialog />
|
||||
<HomeHeader
|
||||
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
||||
searchQuery={searchQuery}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
export function CloseConfirmDialog() {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenPromise = listen("close-confirm-requested", () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
return () => {
|
||||
void unlistenPromise.then((u) => {
|
||||
u();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleMinimize = async () => {
|
||||
setIsOpen(false);
|
||||
try {
|
||||
await invoke("hide_to_tray");
|
||||
} catch (error) {
|
||||
console.error("Failed to hide to tray:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuit = async () => {
|
||||
setIsOpen(false);
|
||||
try {
|
||||
await invoke("confirm_quit");
|
||||
} catch (error) {
|
||||
console.error("Failed to quit app:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("closeConfirm.title")}</DialogTitle>
|
||||
<DialogDescription>{t("closeConfirm.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
void handleMinimize();
|
||||
}}
|
||||
>
|
||||
{t("closeConfirm.minimize")}
|
||||
</RippleButton>
|
||||
<RippleButton
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
void handleQuit();
|
||||
}}
|
||||
>
|
||||
{t("closeConfirm.quit")}
|
||||
</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "Go to Integrations",
|
||||
"goAccount": "Go to Account",
|
||||
"goSettings": "Go to Settings"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "Close Donut Browser?",
|
||||
"description": "Would you like to send the app to the system tray or quit?",
|
||||
"minimize": "Minimize to Tray",
|
||||
"quit": "Quit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "Ir a Integraciones",
|
||||
"goAccount": "Ir a Cuenta",
|
||||
"goSettings": "Ir a Configuración"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "¿Cerrar Donut Browser?",
|
||||
"description": "¿Quieres enviar la aplicación a la bandeja del sistema o salir?",
|
||||
"minimize": "Minimizar a la bandeja",
|
||||
"quit": "Salir"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "Aller à Intégrations",
|
||||
"goAccount": "Aller à Compte",
|
||||
"goSettings": "Aller à Paramètres"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "Fermer Donut Browser ?",
|
||||
"description": "Voulez-vous envoyer l'application dans la zone de notification ou quitter ?",
|
||||
"minimize": "Réduire dans la barre d'état",
|
||||
"quit": "Quitter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "統合へ移動",
|
||||
"goAccount": "アカウントへ移動",
|
||||
"goSettings": "設定へ移動"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "Donut Browser を閉じますか?",
|
||||
"description": "アプリをシステムトレイに格納しますか、それとも終了しますか?",
|
||||
"minimize": "トレイに格納",
|
||||
"quit": "終了"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "통합으로 이동",
|
||||
"goAccount": "계정으로 이동",
|
||||
"goSettings": "설정으로 이동"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "Donut Browser를 닫으시겠습니까?",
|
||||
"description": "앱을 시스템 트레이로 보내시겠습니까, 아니면 종료하시겠습니까?",
|
||||
"minimize": "트레이로 최소화",
|
||||
"quit": "종료"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "Ir para Integrações",
|
||||
"goAccount": "Ir para Conta",
|
||||
"goSettings": "Ir para Configurações"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "Fechar Donut Browser?",
|
||||
"description": "Você deseja enviar o aplicativo para a bandeja do sistema ou sair?",
|
||||
"minimize": "Minimizar para a bandeja",
|
||||
"quit": "Sair"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "Перейти к Интеграциям",
|
||||
"goAccount": "Перейти к Аккаунту",
|
||||
"goSettings": "Перейти к Настройкам"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "Закрыть Donut Browser?",
|
||||
"description": "Свернуть приложение в системный трей или выйти?",
|
||||
"minimize": "Свернуть в трей",
|
||||
"quit": "Выйти"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,5 +1912,11 @@
|
||||
"goIntegrations": "转到集成",
|
||||
"goAccount": "转到账户",
|
||||
"goSettings": "转到设置"
|
||||
},
|
||||
"closeConfirm": {
|
||||
"title": "关闭 Donut Browser?",
|
||||
"description": "您想将应用最小化到系统托盘还是退出?",
|
||||
"minimize": "最小化到托盘",
|
||||
"quit": "退出"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user