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,
/// Delay before starting upload after file stops changing (seconds). 0 disables the delay.
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.
pub language: Option<String>,
}
@ -77,6 +79,7 @@ impl Default for AppConfig {
log_level: LogLevel::Debug,
log_max_files: 5,
sync_delay_seconds: 0,
hide_tray_icon: false,
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
pub fn language(&self) -> Option<String> {
self.config.read().ok().and_then(|c| c.language.clone())

@ -10,6 +10,7 @@ use tauri::utils::{config::WindowEffectsConfig, WindowEffect};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{
tray::TrayIcon,
utils::config::Color,
webview::{WebviewWindow, WebviewWindowBuilder},
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_max_files: config.log_max_files,
sync_delay_seconds: config.sync_delay_seconds,
hide_tray_icon: config.hide_tray_icon,
log_dir: ConfigManager::get_log_dir().display().to_string(),
language: config.language,
})
@ -1214,6 +1216,7 @@ pub struct GeneralSettings {
pub log_level: String,
pub log_max_files: usize,
pub sync_delay_seconds: u64,
pub hide_tray_icon: bool,
pub log_dir: 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())
}
/// 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
#[tauri::command]
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)?;
// 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
// when the language changes.
app.manage(tray);
@ -475,6 +481,7 @@ pub fn run() {
commands::set_log_level,
commands::set_log_max_files,
commands::set_sync_delay_seconds,
commands::set_hide_tray_icon,
commands::set_language,
commands::open_log_folder,
])

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

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

@ -91,6 +91,8 @@
"experimentalFeatures": "Funciones experimentales",
"delaySync": "Retrasar sincronización",
"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",
"openFolder": "Abrir carpeta",
"openSite": "Abrir sitio",

@ -91,6 +91,8 @@
"experimentalFeatures": "Fonctionnalités expérimentales",
"delaySync": "Retarder la synchronisation",
"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",
"openFolder": "Ouvrir le dossier",
"openSite": "Ouvrir le site",

@ -91,6 +91,8 @@
"experimentalFeatures": "Funzionalità sperimentali",
"delaySync": "Ritarda sincronizzazione",
"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",
"openFolder": "Apri cartella",
"openSite": "Apri sito",

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

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

@ -91,6 +91,8 @@
"experimentalFeatures": "Funkcje eksperymentalne",
"delaySync": "Opóźnij synchronizację",
"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ęć",
"openFolder": "Otwórz folder",
"openSite": "Otwórz witrynę",

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

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

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

@ -211,6 +211,7 @@ interface GeneralSettings {
log_level: string;
log_max_files: number;
sync_delay_seconds: number;
hide_tray_icon: boolean;
log_dir: string;
language: string | null;
}
@ -249,6 +250,7 @@ export default function GeneralSection() {
const [logLevel, setLogLevel] = useState("info");
const [logMaxFiles, setLogMaxFiles] = useState(5);
const [syncDelaySeconds, setSyncDelaySeconds] = useState(0);
const [hideTrayIcon, setHideTrayIcon] = useState(false);
const [logDir, setLogDir] = useState("");
const [language, setLanguage] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@ -268,6 +270,7 @@ export default function GeneralSection() {
setLogLevel(settings.log_level);
setLogMaxFiles(settings.log_max_files);
setSyncDelaySeconds(settings.sync_delay_seconds);
setHideTrayIcon(settings.hide_tray_icon);
setLogDir(settings.log_dir);
setLanguage(settings.language);
} 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 previousValue = notifyCredentialExpired;
setNotifyCredentialExpired(checked);
@ -417,6 +431,14 @@ export default function GeneralSection() {
checked={fastPopupLaunch}
onChange={handleFastPopupLaunchChange}
disabled={loading}
isLast={false}
/>
<SettingItem
title={t("settings.hideTrayIcon")}
description={t("settings.hideTrayIconDescription")}
checked={hideTrayIcon}
onChange={handleHideTrayIconChange}
disabled={loading}
isLast={true}
/>
</SettingsGroup>

Loading…
Cancel
Save