feat(desktop): hide-tray-icon setting (#149)

AppConfig.hide_tray_icon persists the preference; setup_tray applies it
post-build (TrayIconBuilder has no visibility option), and the
set_hide_tray_icon command toggles the live icon via managed state.
Settings UI toggle in the launch group + all 11 locales.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3587/head
Tomas Dvorak 2 weeks ago
parent 50b2b104a5
commit def78e9c04

@ -62,6 +62,8 @@ pub struct AppConfig {
pub log_max_files: usize, pub log_max_files: usize,
/// Delay before starting upload after file stops changing (seconds). 0 disables the delay. /// Delay before starting upload after file stops changing (seconds). 0 disables the delay.
pub sync_delay_seconds: u64, pub sync_delay_seconds: u64,
/// Whether to hide the system tray icon (popup stays reachable via shortcuts/relaunch)
pub hide_tray_icon: bool,
/// Language/locale setting (e.g., "en-US", "zh-CN"). None means use system default. /// Language/locale setting (e.g., "en-US", "zh-CN"). None means use system default.
pub language: Option<String>, pub language: Option<String>,
} }
@ -77,6 +79,7 @@ impl Default for AppConfig {
log_level: LogLevel::Debug, log_level: LogLevel::Debug,
log_max_files: 5, log_max_files: 5,
sync_delay_seconds: 0, sync_delay_seconds: 0,
hide_tray_icon: false,
language: None, language: None,
} }
} }
@ -299,6 +302,21 @@ impl ConfigManager {
}) })
} }
/// Whether the system tray icon is hidden
pub fn hide_tray_icon(&self) -> bool {
self.config
.read()
.map(|c| c.hide_tray_icon)
.unwrap_or(false)
}
/// Set whether the system tray icon is hidden
pub fn set_hide_tray_icon(&self, hide: bool) -> Result<()> {
self.update(|config| {
config.hide_tray_icon = hide;
})
}
/// Get the language setting /// Get the language setting
pub fn language(&self) -> Option<String> { pub fn language(&self) -> Option<String> {
self.config.read().ok().and_then(|c| c.language.clone()) self.config.read().ok().and_then(|c| c.language.clone())

@ -10,6 +10,7 @@ use tauri::utils::{config::WindowEffectsConfig, WindowEffect};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
use tauri::{ use tauri::{
tray::TrayIcon,
utils::config::Color, utils::config::Color,
webview::{WebviewWindow, WebviewWindowBuilder}, webview::{WebviewWindow, WebviewWindowBuilder},
AppHandle, Manager, State, WebviewUrl, AppHandle, Manager, State, WebviewUrl,
@ -1200,6 +1201,7 @@ pub async fn get_general_settings() -> CommandResult<GeneralSettings> {
log_level: config.log_level.as_str().to_string(), log_level: config.log_level.as_str().to_string(),
log_max_files: config.log_max_files, log_max_files: config.log_max_files,
sync_delay_seconds: config.sync_delay_seconds, sync_delay_seconds: config.sync_delay_seconds,
hide_tray_icon: config.hide_tray_icon,
log_dir: ConfigManager::get_log_dir().display().to_string(), log_dir: ConfigManager::get_log_dir().display().to_string(),
language: config.language, language: config.language,
}) })
@ -1214,6 +1216,7 @@ pub struct GeneralSettings {
pub log_level: String, pub log_level: String,
pub log_max_files: usize, pub log_max_files: usize,
pub sync_delay_seconds: u64, pub sync_delay_seconds: u64,
pub hide_tray_icon: bool,
pub log_dir: String, pub log_dir: String,
pub language: Option<String>, pub language: Option<String>,
} }
@ -1253,6 +1256,18 @@ pub async fn set_sync_delay_seconds(seconds: u64) -> CommandResult<()> {
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
/// Set whether the system tray icon is hidden and apply it live
#[tauri::command]
pub async fn set_hide_tray_icon(app: AppHandle, hide: bool) -> CommandResult<()> {
ConfigManager::get()
.set_hide_tray_icon(hide)
.map_err(|e| e.to_string())?;
if let Some(tray) = app.try_state::<TrayIcon>() {
tray.set_visible(!hide).map_err(|e| e.to_string())?;
}
Ok(())
}
/// Set language setting and update rust_i18n locale /// Set language setting and update rust_i18n locale
#[tauri::command] #[tauri::command]
pub async fn set_language(app: AppHandle, language: Option<String>) -> CommandResult<()> { pub async fn set_language(app: AppHandle, language: Option<String>) -> CommandResult<()> {

@ -366,6 +366,12 @@ fn setup_tray(app: &tauri::App) -> anyhow::Result<()> {
}) })
.build(app)?; .build(app)?;
// Honour the hide-tray-icon setting (TrayIconBuilder has no visibility
// option, so it is applied right after construction).
if ConfigManager::get().hide_tray_icon() {
let _ = tray.set_visible(false);
}
// Keep the tray icon in Tauri's managed state so we can update its menu // Keep the tray icon in Tauri's managed state so we can update its menu
// when the language changes. // when the language changes.
app.manage(tray); app.manage(tray);
@ -475,6 +481,7 @@ pub fn run() {
commands::set_log_level, commands::set_log_level,
commands::set_log_max_files, commands::set_log_max_files,
commands::set_sync_delay_seconds, commands::set_sync_delay_seconds,
commands::set_hide_tray_icon,
commands::set_language, commands::set_language,
commands::open_log_folder, commands::open_log_folder,
]) ])

@ -91,6 +91,8 @@
"experimentalFeatures": "Experimentelle Funktionen", "experimentalFeatures": "Experimentelle Funktionen",
"delaySync": "Synchronisierung verzögern", "delaySync": "Synchronisierung verzögern",
"delaySyncDescription": "Die Synchronisierungsaufgabe wartet, bis die Datei nicht mehr geändert wird", "delaySyncDescription": "Die Synchronisierungsaufgabe wartet, bis die Datei nicht mehr geändert wird",
"hideTrayIcon": "Infobereichssymbol ausblenden",
"hideTrayIconDescription": "Im Hintergrund ohne Infobereichssymbol ausführen",
"storage": "Speicher", "storage": "Speicher",
"openFolder": "Ordner öffnen", "openFolder": "Ordner öffnen",
"openSite": "Website öffnen", "openSite": "Website öffnen",

@ -91,6 +91,8 @@
"experimentalFeatures": "Experimental features", "experimentalFeatures": "Experimental features",
"delaySync": "Delay sync", "delaySync": "Delay sync",
"delaySyncDescription": "Sync task wait until file stop changing", "delaySyncDescription": "Sync task wait until file stop changing",
"hideTrayIcon": "Hide tray icon",
"hideTrayIconDescription": "Run in the background without a system tray icon",
"storage": "Storage", "storage": "Storage",
"openFolder": "Open folder", "openFolder": "Open folder",
"openSite": "Open site", "openSite": "Open site",

@ -91,6 +91,8 @@
"experimentalFeatures": "Funciones experimentales", "experimentalFeatures": "Funciones experimentales",
"delaySync": "Retrasar sincronización", "delaySync": "Retrasar sincronización",
"delaySyncDescription": "La tarea de sincronización espera hasta que el archivo deje de cambiar", "delaySyncDescription": "La tarea de sincronización espera hasta que el archivo deje de cambiar",
"hideTrayIcon": "Ocultar icono de bandeja",
"hideTrayIconDescription": "Ejecutar en segundo plano sin icono en la bandeja del sistema",
"storage": "Almacenamiento", "storage": "Almacenamiento",
"openFolder": "Abrir carpeta", "openFolder": "Abrir carpeta",
"openSite": "Abrir sitio", "openSite": "Abrir sitio",

@ -91,6 +91,8 @@
"experimentalFeatures": "Fonctionnalités expérimentales", "experimentalFeatures": "Fonctionnalités expérimentales",
"delaySync": "Retarder la synchronisation", "delaySync": "Retarder la synchronisation",
"delaySyncDescription": "La tâche de synchronisation attend que le fichier cesse d'être modifié", "delaySyncDescription": "La tâche de synchronisation attend que le fichier cesse d'être modifié",
"hideTrayIcon": "Masquer l'icône de la zone de notification",
"hideTrayIconDescription": "S'exécuter en arrière-plan sans icône dans la zone de notification",
"storage": "Stockage", "storage": "Stockage",
"openFolder": "Ouvrir le dossier", "openFolder": "Ouvrir le dossier",
"openSite": "Ouvrir le site", "openSite": "Ouvrir le site",

@ -91,6 +91,8 @@
"experimentalFeatures": "Funzionalità sperimentali", "experimentalFeatures": "Funzionalità sperimentali",
"delaySync": "Ritarda sincronizzazione", "delaySync": "Ritarda sincronizzazione",
"delaySyncDescription": "L'attività di sincronizzazione attende che il file smetta di cambiare", "delaySyncDescription": "L'attività di sincronizzazione attende che il file smetta di cambiare",
"hideTrayIcon": "Nascondi icona nella barra delle applicazioni",
"hideTrayIconDescription": "Esegui in background senza icona nell'area di notifica",
"storage": "Archiviazione", "storage": "Archiviazione",
"openFolder": "Apri cartella", "openFolder": "Apri cartella",
"openSite": "Apri sito", "openSite": "Apri sito",

@ -91,6 +91,8 @@
"experimentalFeatures": "実験的な機能", "experimentalFeatures": "実験的な機能",
"delaySync": "同期を遅延", "delaySync": "同期を遅延",
"delaySyncDescription": "同期タスクはファイルの変更が止まるまで待機します", "delaySyncDescription": "同期タスクはファイルの変更が止まるまで待機します",
"hideTrayIcon": "トレイアイコンを非表示",
"hideTrayIconDescription": "システムトレイアイコンなしでバックグラウンド実行",
"storage": "ストレージ", "storage": "ストレージ",
"openFolder": "フォルダを開く", "openFolder": "フォルダを開く",
"openSite": "サイトを開く", "openSite": "サイトを開く",

@ -91,6 +91,8 @@
"experimentalFeatures": "실험적 기능", "experimentalFeatures": "실험적 기능",
"delaySync": "동기화 지연", "delaySync": "동기화 지연",
"delaySyncDescription": "동기화 작업은 파일 변경이 멈출 때까지 기다립니다", "delaySyncDescription": "동기화 작업은 파일 변경이 멈출 때까지 기다립니다",
"hideTrayIcon": "트레이 아이콘 숨기기",
"hideTrayIconDescription": "시스템 트레이 아이콘 없이 백그라운드에서 실행",
"storage": "저장소", "storage": "저장소",
"openFolder": "폴더 열기", "openFolder": "폴더 열기",
"openSite": "사이트 열기", "openSite": "사이트 열기",

@ -91,6 +91,8 @@
"experimentalFeatures": "Funkcje eksperymentalne", "experimentalFeatures": "Funkcje eksperymentalne",
"delaySync": "Opóźnij synchronizację", "delaySync": "Opóźnij synchronizację",
"delaySyncDescription": "Zadanie synchronizacji czeka, aż plik przestanie się zmieniać", "delaySyncDescription": "Zadanie synchronizacji czeka, aż plik przestanie się zmieniać",
"hideTrayIcon": "Ukryj ikonę w zasobniku",
"hideTrayIconDescription": "Działaj w tle bez ikony w zasobniku systemowym",
"storage": "Pamięć", "storage": "Pamięć",
"openFolder": "Otwórz folder", "openFolder": "Otwórz folder",
"openSite": "Otwórz witrynę", "openSite": "Otwórz witrynę",

@ -91,6 +91,8 @@
"experimentalFeatures": "Экспериментальные функции", "experimentalFeatures": "Экспериментальные функции",
"delaySync": "Задержка синхронизации", "delaySync": "Задержка синхронизации",
"delaySyncDescription": "Задача синхронизации ждёт, пока файл перестанет изменяться", "delaySyncDescription": "Задача синхронизации ждёт, пока файл перестанет изменяться",
"hideTrayIcon": "Скрыть значок в трее",
"hideTrayIconDescription": "Работать в фоне без значка в системном трее",
"storage": "Хранилище", "storage": "Хранилище",
"openFolder": "Открыть папку", "openFolder": "Открыть папку",
"openSite": "Открыть сайт", "openSite": "Открыть сайт",

@ -91,6 +91,8 @@
"experimentalFeatures": "实验性功能", "experimentalFeatures": "实验性功能",
"delaySync": "延迟同步", "delaySync": "延迟同步",
"delaySyncDescription": "同步任务等待文件停止更改后再上传", "delaySyncDescription": "同步任务等待文件停止更改后再上传",
"hideTrayIcon": "隐藏托盘图标",
"hideTrayIconDescription": "在后台运行而不显示系统托盘图标",
"storage": "存储空间", "storage": "存储空间",
"openFolder": "打开文件夹", "openFolder": "打开文件夹",
"openSite": "打开站点", "openSite": "打开站点",

@ -91,6 +91,8 @@
"experimentalFeatures": "實驗性功能", "experimentalFeatures": "實驗性功能",
"delaySync": "延遲同步", "delaySync": "延遲同步",
"delaySyncDescription": "同步任務會等待檔案停止變更", "delaySyncDescription": "同步任務會等待檔案停止變更",
"hideTrayIcon": "隱藏系統匣圖示",
"hideTrayIconDescription": "在背景執行而不顯示系統匣圖示",
"storage": "儲存空間", "storage": "儲存空間",
"openFolder": "開啟資料夾", "openFolder": "開啟資料夾",
"openSite": "開啟網站", "openSite": "開啟網站",

@ -211,6 +211,7 @@ interface GeneralSettings {
log_level: string; log_level: string;
log_max_files: number; log_max_files: number;
sync_delay_seconds: number; sync_delay_seconds: number;
hide_tray_icon: boolean;
log_dir: string; log_dir: string;
language: string | null; language: string | null;
} }
@ -249,6 +250,7 @@ export default function GeneralSection() {
const [logLevel, setLogLevel] = useState("info"); const [logLevel, setLogLevel] = useState("info");
const [logMaxFiles, setLogMaxFiles] = useState(5); const [logMaxFiles, setLogMaxFiles] = useState(5);
const [syncDelaySeconds, setSyncDelaySeconds] = useState(0); const [syncDelaySeconds, setSyncDelaySeconds] = useState(0);
const [hideTrayIcon, setHideTrayIcon] = useState(false);
const [logDir, setLogDir] = useState(""); const [logDir, setLogDir] = useState("");
const [language, setLanguage] = useState<string | null>(null); const [language, setLanguage] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -268,6 +270,7 @@ export default function GeneralSection() {
setLogLevel(settings.log_level); setLogLevel(settings.log_level);
setLogMaxFiles(settings.log_max_files); setLogMaxFiles(settings.log_max_files);
setSyncDelaySeconds(settings.sync_delay_seconds); setSyncDelaySeconds(settings.sync_delay_seconds);
setHideTrayIcon(settings.hide_tray_icon);
setLogDir(settings.log_dir); setLogDir(settings.log_dir);
setLanguage(settings.language); setLanguage(settings.language);
} catch (error) { } catch (error) {
@ -293,6 +296,17 @@ export default function GeneralSection() {
} }
}; };
const handleHideTrayIconChange = async (checked: boolean) => {
const previousValue = hideTrayIcon;
setHideTrayIcon(checked);
try {
await invoke("set_hide_tray_icon", { hide: checked });
} catch (error) {
console.error("Failed to change tray icon setting:", error);
setHideTrayIcon(previousValue);
}
};
const handleNotifyCredentialExpiredChange = async (checked: boolean) => { const handleNotifyCredentialExpiredChange = async (checked: boolean) => {
const previousValue = notifyCredentialExpired; const previousValue = notifyCredentialExpired;
setNotifyCredentialExpired(checked); setNotifyCredentialExpired(checked);
@ -417,6 +431,14 @@ export default function GeneralSection() {
checked={fastPopupLaunch} checked={fastPopupLaunch}
onChange={handleFastPopupLaunchChange} onChange={handleFastPopupLaunchChange}
disabled={loading} disabled={loading}
isLast={false}
/>
<SettingItem
title={t("settings.hideTrayIcon")}
description={t("settings.hideTrayIconDescription")}
checked={hideTrayIcon}
onChange={handleHideTrayIconChange}
disabled={loading}
isLast={true} isLast={true}
/> />
</SettingsGroup> </SettingsGroup>

Loading…
Cancel
Save