diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fb3920b7..7c20467a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -213,6 +213,12 @@ fn initialize_core_logic(app_handle: &AppHandle) { // Initialize tray menu with idle state utils::update_tray_menu(app_handle, &utils::TrayIconState::Idle, None); + // Apply show_tray_icon setting + let settings = settings::get_settings(app_handle); + if !settings.show_tray_icon { + tray::set_tray_visibility(app_handle, false); + } + // Refresh tray menu when model state changes let app_handle_for_listener = app_handle.clone(); app_handle.listen("model-state-changed", move |_| { @@ -289,6 +295,7 @@ pub fn run() { shortcut::change_update_checks_setting, shortcut::change_keyboard_implementation_setting, shortcut::get_keyboard_implementation, + shortcut::change_show_tray_icon_setting, shortcut::handy_keys::start_handy_keys_recording, shortcut::handy_keys::stop_handy_keys_recording, trigger_update_check, @@ -419,6 +426,12 @@ pub fn run() { }) .on_window_event(|window, event| match event { tauri::WindowEvent::CloseRequested { api, .. } => { + let settings = get_settings(&window.app_handle()); + // If tray icon is hidden, quit the app + if !settings.show_tray_icon { + window.app_handle().exit(0); + return; + } api.prevent_close(); let _res = window.hide(); #[cfg(target_os = "macos")] diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 44402bc1..545f73ed 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -315,6 +315,8 @@ pub struct AppSettings { pub experimental_enabled: bool, #[serde(default)] pub keyboard_implementation: KeyboardImplementation, + #[serde(default = "default_show_tray_icon")] + pub show_tray_icon: bool, #[serde(default = "default_paste_delay_ms")] pub paste_delay_ms: u64, } @@ -396,6 +398,10 @@ fn default_app_language() -> String { .unwrap_or_else(|| "en".to_string()) } +fn default_show_tray_icon() -> bool { + true +} + fn default_post_process_provider_id() -> String { "openai".to_string() } @@ -631,6 +637,7 @@ pub fn get_default_settings() -> AppSettings { app_language: default_app_language(), experimental_enabled: false, keyboard_implementation: KeyboardImplementation::default(), + show_tray_icon: default_show_tray_icon(), paste_delay_ms: default_paste_delay_ms(), } } diff --git a/src-tauri/src/shortcut/mod.rs b/src-tauri/src/shortcut/mod.rs index 6ff5a04b..1225288e 100644 --- a/src-tauri/src/shortcut/mod.rs +++ b/src-tauri/src/shortcut/mod.rs @@ -978,3 +978,16 @@ pub fn change_app_language_setting(app: AppHandle, language: String) -> Result<( Ok(()) } + +#[tauri::command] +#[specta::specta] +pub fn change_show_tray_icon_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.show_tray_icon = enabled; + settings::write_settings(&app, settings); + + // Apply change immediately + tray::set_tray_visibility(&app, enabled); + + Ok(()) +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index e5beaa06..31e0effa 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -187,6 +187,15 @@ fn last_transcript_text(entry: &HistoryEntry) -> &str { .unwrap_or(&entry.transcription_text) } +pub fn set_tray_visibility(app: &AppHandle, visible: bool) { + let tray = app.state::(); + if let Err(e) = tray.set_visible(visible) { + error!("Failed to set tray visibility: {}", e); + } else { + info!("Tray visibility set to: {}", visible); + } +} + pub fn copy_last_transcript(app: &AppHandle) { let history_manager = app.state::>(); let entry = match history_manager.get_latest_entry() { diff --git a/src/bindings.ts b/src/bindings.ts index 0cbe4398..d4024eb2 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -296,6 +296,14 @@ async changeKeyboardImplementationSetting(implementation: string) : Promise { return await TAURI_INVOKE("get_keyboard_implementation"); }, +async changeShowTrayIconSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_show_tray_icon_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, /** * Start key recording mode */ @@ -695,7 +703,7 @@ async isLaptop() : Promise> { /** user-defined types **/ -export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; keyboard_implementation?: KeyboardImplementation; paste_delay_ms?: number } +export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number } export type AudioDevice = { index: string; name: string; is_default: boolean } export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null } export type ClipboardHandling = "dont_modify" | "copy_to_clipboard" diff --git a/src/components/settings/ShowTrayIcon.tsx b/src/components/settings/ShowTrayIcon.tsx new file mode 100644 index 00000000..0f0b51f6 --- /dev/null +++ b/src/components/settings/ShowTrayIcon.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { ToggleSwitch } from "../ui/ToggleSwitch"; +import { useSettings } from "../../hooks/useSettings"; + +interface ShowTrayIconProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const ShowTrayIcon: React.FC = React.memo( + ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); + const { getSetting, updateSetting, isUpdating } = useSettings(); + + const showTrayIcon = getSetting("show_tray_icon") ?? true; + + return ( + updateSetting("show_tray_icon", enabled)} + isUpdating={isUpdating("show_tray_icon")} + label={t("settings.advanced.showTrayIcon.label")} + description={t("settings.advanced.showTrayIcon.description")} + descriptionMode={descriptionMode} + grouped={grouped} + tooltipPosition="bottom" + /> + ); + }, +); diff --git a/src/components/settings/advanced/AdvancedSettings.tsx b/src/components/settings/advanced/AdvancedSettings.tsx index fa220ea6..e8d3630b 100644 --- a/src/components/settings/advanced/AdvancedSettings.tsx +++ b/src/components/settings/advanced/AdvancedSettings.tsx @@ -6,6 +6,7 @@ import { CustomWords } from "../CustomWords"; import { SettingsGroup } from "../../ui/SettingsGroup"; import { StartHidden } from "../StartHidden"; import { AutostartToggle } from "../AutostartToggle"; +import { ShowTrayIcon } from "../ShowTrayIcon"; import { PasteMethodSetting } from "../PasteMethod"; import { ClipboardHandlingSetting } from "../ClipboardHandling"; import { PostProcessingToggle } from "../PostProcessingToggle"; @@ -26,6 +27,7 @@ export const AdvancedSettings: React.FC = () => { + diff --git a/src/i18n/locales/ar/translation.json b/src/i18n/locales/ar/translation.json index a83ba642..769ecd3e 100644 --- a/src/i18n/locales/ar/translation.json +++ b/src/i18n/locales/ar/translation.json @@ -198,6 +198,10 @@ "label": "التشغيل عند بدء التشغيل", "description": ".بدء تشغيل Handy تلقائياً عند تسجيل الدخول إلى جهاز الكمبيوتر الخاص بك" }, + "showTrayIcon": { + "label": "إظهار أيقونة شريط النظام", + "description": ".عرض أيقونة Handy في شريط النظام" + }, "overlay": { "title": "موقع التراكب", "description": "عرض تراكب الملاحظات المرئية أثناء التسجيل والتفريغ. على نظام Linux يوصى بـ 'بلا'.", diff --git a/src/i18n/locales/cs/translation.json b/src/i18n/locales/cs/translation.json index e4a67cc4..e4a6d215 100644 --- a/src/i18n/locales/cs/translation.json +++ b/src/i18n/locales/cs/translation.json @@ -220,6 +220,10 @@ "label": "Spouštět při startu", "description": "Automaticky spustit Handy po přihlášení do počítače." }, + "showTrayIcon": { + "label": "Zobrazit ikonu v systémové liště", + "description": "Zobrazit ikonu Handy v systémové liště." + }, "overlay": { "title": "Pozice překryvu", "description": "Zobrazovat vizuální překryv během nahrávání a přepisu. Na Linuxu je doporučeno 'Žádné'.", diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 31ae54c3..d0701afb 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -220,6 +220,10 @@ "label": "Beim Start ausführen", "description": "Handy automatisch beim Anmelden starten." }, + "showTrayIcon": { + "label": "Taskleistensymbol anzeigen", + "description": "Das Handy-Symbol in der Taskleiste anzeigen." + }, "overlay": { "title": "Overlay-Position", "description": "Visuelles Feedback-Overlay während Aufnahme und Transkription anzeigen. Unter Linux wird 'Keine' empfohlen.", diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 714e2bf2..67473566 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -220,6 +220,10 @@ "label": "Launch on Startup", "description": "Automatically start Handy when you log in to your computer." }, + "showTrayIcon": { + "label": "Show Tray Icon", + "description": "Display the Handy icon in the system tray." + }, "overlay": { "title": "Overlay Position", "description": "Display visual feedback overlay during recording and transcription. On Linux 'None' is recommended.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 93cc64ee..de0833b2 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -220,6 +220,10 @@ "label": "Iniciar al Arranque", "description": "Iniciar Handy automáticamente cuando inicies sesión en tu computadora." }, + "showTrayIcon": { + "label": "Mostrar Icono de Bandeja", + "description": "Mostrar el icono de Handy en la bandeja del sistema." + }, "overlay": { "title": "Posición de Superposición", "description": "Mostrar superposición de retroalimentación visual durante la grabación y transcripción. En Linux se recomienda 'Ninguna'.", diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index b2099f1c..66f81c56 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -220,6 +220,10 @@ "label": "Lancer au démarrage", "description": "Démarrer automatiquement Handy lorsque vous vous connectez à votre ordinateur." }, + "showTrayIcon": { + "label": "Afficher l'icône de la barre", + "description": "Afficher l'icône de Handy dans la barre système." + }, "overlay": { "title": "Position de la fenêtre d'enregistrement", "description": "Afficher un retour visuel pendant l'enregistrement et la transcription. Sur Linux, 'Aucune' est recommandé.", diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index 2116f5f2..f32be5ce 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -220,6 +220,10 @@ "label": "Avvia all'Accensione", "description": "Avvia Handy automaticamente quando accedi al computer." }, + "showTrayIcon": { + "label": "Mostra icona nella barra di sistema", + "description": "Mostra l'icona di Handy nella barra di sistema." + }, "overlay": { "title": "Posizione della Sovrimpressione", "description": "Mostra un feedback visivo in sovrimpressione durante la registrazione e la trascrizione. Su Linux si raccomanda 'Nessuna'.", diff --git a/src/i18n/locales/ja/translation.json b/src/i18n/locales/ja/translation.json index 6af229d8..04860a7d 100644 --- a/src/i18n/locales/ja/translation.json +++ b/src/i18n/locales/ja/translation.json @@ -220,6 +220,10 @@ "label": "起動時に実行", "description": "コンピューターにログインしたときにHandyを自動的に起動。" }, + "showTrayIcon": { + "label": "トレイアイコンを表示", + "description": "システムトレイにHandyのアイコンを表示します。" + }, "overlay": { "title": "オーバーレイ位置", "description": "録音と文字起こし中に視覚的なフィードバックオーバーレイを表示。Linuxでは「なし」を推奨。", diff --git a/src/i18n/locales/ko/translation.json b/src/i18n/locales/ko/translation.json index 9712fbf5..60086849 100644 --- a/src/i18n/locales/ko/translation.json +++ b/src/i18n/locales/ko/translation.json @@ -220,6 +220,10 @@ "label": "시작 시 실행", "description": "컴퓨터 로그인 시 Handy를 자동으로 시작합니다." }, + "showTrayIcon": { + "label": "트레이 아이콘 표시", + "description": "시스템 트레이에 Handy 아이콘을 표시합니다." + }, "overlay": { "title": "오버레이 위치", "description": "녹음 및 전사 중 시각적 피드백 오버레이를 표시합니다. Linux에서는 '없음'을 권장합니다.", diff --git a/src/i18n/locales/pl/translation.json b/src/i18n/locales/pl/translation.json index 5244ab33..410838da 100644 --- a/src/i18n/locales/pl/translation.json +++ b/src/i18n/locales/pl/translation.json @@ -220,6 +220,10 @@ "label": "Uruchamiaj przy starcie", "description": "Automatycznie uruchamiaj Handy po zalogowaniu." }, + "showTrayIcon": { + "label": "Pokaż ikonę w zasobniku", + "description": "Wyświetlaj ikonę Handy w zasobniku systemowym." + }, "overlay": { "title": "Pozycja nakładki", "description": "Wyświetlaj wizualną nakładkę podczas nagrywania i transkrypcji. Na Linuxie zalecane 'Brak'.", diff --git a/src/i18n/locales/pt/translation.json b/src/i18n/locales/pt/translation.json index df9f3e4b..81a50f8b 100644 --- a/src/i18n/locales/pt/translation.json +++ b/src/i18n/locales/pt/translation.json @@ -220,6 +220,10 @@ "label": "Iniciar na Inicialização", "description": "Iniciar automaticamente o Handy quando você fizer login no seu computador." }, + "showTrayIcon": { + "label": "Mostrar ícone na bandeja", + "description": "Exibir o ícone do Handy na bandeja do sistema." + }, "overlay": { "title": "Posição da Sobreposição", "description": "Exibir sobreposição de feedback visual durante gravação e transcrição. No Linux, 'Nenhum' é recomendado.", diff --git a/src/i18n/locales/ru/translation.json b/src/i18n/locales/ru/translation.json index 518b744e..20ba7945 100644 --- a/src/i18n/locales/ru/translation.json +++ b/src/i18n/locales/ru/translation.json @@ -220,6 +220,10 @@ "label": "Запуск при запуске", "description": "Автоматически запускать Handy при входе в систему." }, + "showTrayIcon": { + "label": "Показать значок в трее", + "description": "Отображать значок Handy в системном трее." + }, "overlay": { "title": "Позиция наложения", "description": "Отображение наложения визуальной обратной связи во время записи и транскрипции. В Linux рекомендуется выбрать «Нет».", diff --git a/src/i18n/locales/tr/translation.json b/src/i18n/locales/tr/translation.json index 3c8a02c5..ab1d9a0e 100644 --- a/src/i18n/locales/tr/translation.json +++ b/src/i18n/locales/tr/translation.json @@ -220,6 +220,10 @@ "label": "Başlangıçta Çalıştır", "description": "Bilgisayara giriş yaptığınızda Handy otomatik olarak başlatılır." }, + "showTrayIcon": { + "label": "Tepsi simgesini göster", + "description": "Handy simgesini sistem tepsisinde göster." + }, "overlay": { "title": "Overlay Konumu", "description": "Kayıt ve transkripsiyon sırasında görsel geri bildirim kaplamasını gösterir. Linux'ta 'Yok' önerilir.", diff --git a/src/i18n/locales/uk/translation.json b/src/i18n/locales/uk/translation.json index db9b81aa..7ff0f69d 100644 --- a/src/i18n/locales/uk/translation.json +++ b/src/i18n/locales/uk/translation.json @@ -220,6 +220,10 @@ "label": "Запуск при старті системи", "description": "Автоматично запускати Handy при вході в систему" }, + "showTrayIcon": { + "label": "Показати значок у треї", + "description": "Відображати значок Handy у системному треї." + }, "overlay": { "title": "Позиція оверлею", "description": "Показувати візуальний оверлей під час запису та транскрипції. На Linux рекомендовано «Немає»", diff --git a/src/i18n/locales/vi/translation.json b/src/i18n/locales/vi/translation.json index da530f44..2e41fdd9 100644 --- a/src/i18n/locales/vi/translation.json +++ b/src/i18n/locales/vi/translation.json @@ -220,6 +220,10 @@ "label": "Khởi động cùng hệ thống", "description": "Tự động khởi động Handy khi bạn đăng nhập vào máy tính." }, + "showTrayIcon": { + "label": "Hiển thị biểu tượng khay", + "description": "Hiển thị biểu tượng Handy trong khay hệ thống." + }, "overlay": { "title": "Vị trí lớp phủ", "description": "Hiển thị lớp phủ phản hồi trực quan trong quá trình ghi âm và chuyển đổi. Trên Linux, 'Không có' được khuyến nghị.", diff --git a/src/i18n/locales/zh/translation.json b/src/i18n/locales/zh/translation.json index 4f240f64..9c5f8e61 100644 --- a/src/i18n/locales/zh/translation.json +++ b/src/i18n/locales/zh/translation.json @@ -220,6 +220,10 @@ "label": "开机启动", "description": "登录计算机时自动启动 Handy。" }, + "showTrayIcon": { + "label": "显示托盘图标", + "description": "在系统托盘中显示 Handy 图标。" + }, "overlay": { "title": "悬浮窗位置", "description": "在录制和转录期间显示可视反馈悬浮窗。在 Linux 上建议选择「无」。", diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 620ab705..63908602 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -127,6 +127,8 @@ const settingUpdaters: { app_language: (value) => commands.changeAppLanguageSetting(value as string), experimental_enabled: (value) => commands.changeExperimentalEnabledSetting(value as boolean), + show_tray_icon: (value) => + commands.changeShowTrayIconSetting(value as boolean), }; export const useSettingsStore = create()(