Add show tray icon setting (#667)
* feat: add show tray icon setting Allow hiding system tray icon via Settings > Advanced. When hidden, closing window quits app instead of hiding. * i8n --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
ea4b270f85
commit
f2c306eab6
24 changed files with 150 additions and 1 deletions
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<TrayIcon>();
|
||||
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::<Arc<HistoryManager>>();
|
||||
let entry = match history_manager.get_latest_entry() {
|
||||
|
|
|
|||
|
|
@ -296,6 +296,14 @@ async changeKeyboardImplementationSetting(implementation: string) : Promise<Resu
|
|||
async getKeyboardImplementation() : Promise<string> {
|
||||
return await TAURI_INVOKE("get_keyboard_implementation");
|
||||
},
|
||||
async changeShowTrayIconSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
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<Result<boolean, string>> {
|
|||
|
||||
/** 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"
|
||||
|
|
|
|||
31
src/components/settings/ShowTrayIcon.tsx
Normal file
31
src/components/settings/ShowTrayIcon.tsx
Normal file
|
|
@ -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<ShowTrayIconProps> = React.memo(
|
||||
({ descriptionMode = "tooltip", grouped = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||
|
||||
const showTrayIcon = getSetting("show_tray_icon") ?? true;
|
||||
|
||||
return (
|
||||
<ToggleSwitch
|
||||
checked={showTrayIcon}
|
||||
onChange={(enabled) => 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"
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -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 = () => {
|
|||
<SettingsGroup title={t("settings.advanced.groups.app")}>
|
||||
<StartHidden descriptionMode="tooltip" grouped={true} />
|
||||
<AutostartToggle descriptionMode="tooltip" grouped={true} />
|
||||
<ShowTrayIcon descriptionMode="tooltip" grouped={true} />
|
||||
<ShowOverlay descriptionMode="tooltip" grouped={true} />
|
||||
<ModelUnloadTimeoutSetting descriptionMode="tooltip" grouped={true} />
|
||||
<ExperimentalToggle descriptionMode="tooltip" grouped={true} />
|
||||
|
|
|
|||
|
|
@ -198,6 +198,10 @@
|
|||
"label": "التشغيل عند بدء التشغيل",
|
||||
"description": ".بدء تشغيل Handy تلقائياً عند تسجيل الدخول إلى جهاز الكمبيوتر الخاص بك"
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"label": "إظهار أيقونة شريط النظام",
|
||||
"description": ".عرض أيقونة Handy في شريط النظام"
|
||||
},
|
||||
"overlay": {
|
||||
"title": "موقع التراكب",
|
||||
"description": "عرض تراكب الملاحظات المرئية أثناء التسجيل والتفريغ. على نظام Linux يوصى بـ 'بلا'.",
|
||||
|
|
|
|||
|
|
@ -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é'.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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'.",
|
||||
|
|
|
|||
|
|
@ -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é.",
|
||||
|
|
|
|||
|
|
@ -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'.",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"label": "起動時に実行",
|
||||
"description": "コンピューターにログインしたときにHandyを自動的に起動。"
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"label": "トレイアイコンを表示",
|
||||
"description": "システムトレイにHandyのアイコンを表示します。"
|
||||
},
|
||||
"overlay": {
|
||||
"title": "オーバーレイ位置",
|
||||
"description": "録音と文字起こし中に視覚的なフィードバックオーバーレイを表示。Linuxでは「なし」を推奨。",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"label": "시작 시 실행",
|
||||
"description": "컴퓨터 로그인 시 Handy를 자동으로 시작합니다."
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"label": "트레이 아이콘 표시",
|
||||
"description": "시스템 트레이에 Handy 아이콘을 표시합니다."
|
||||
},
|
||||
"overlay": {
|
||||
"title": "오버레이 위치",
|
||||
"description": "녹음 및 전사 중 시각적 피드백 오버레이를 표시합니다. Linux에서는 '없음'을 권장합니다.",
|
||||
|
|
|
|||
|
|
@ -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'.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"label": "Запуск при запуске",
|
||||
"description": "Автоматически запускать Handy при входе в систему."
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"label": "Показать значок в трее",
|
||||
"description": "Отображать значок Handy в системном трее."
|
||||
},
|
||||
"overlay": {
|
||||
"title": "Позиция наложения",
|
||||
"description": "Отображение наложения визуальной обратной связи во время записи и транскрипции. В Linux рекомендуется выбрать «Нет».",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"label": "Запуск при старті системи",
|
||||
"description": "Автоматично запускати Handy при вході в систему"
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"label": "Показати значок у треї",
|
||||
"description": "Відображати значок Handy у системному треї."
|
||||
},
|
||||
"overlay": {
|
||||
"title": "Позиція оверлею",
|
||||
"description": "Показувати візуальний оверлей під час запису та транскрипції. На Linux рекомендовано «Немає»",
|
||||
|
|
|
|||
|
|
@ -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ị.",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"label": "开机启动",
|
||||
"description": "登录计算机时自动启动 Handy。"
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"label": "显示托盘图标",
|
||||
"description": "在系统托盘中显示 Handy 图标。"
|
||||
},
|
||||
"overlay": {
|
||||
"title": "悬浮窗位置",
|
||||
"description": "在录制和转录期间显示可视反馈悬浮窗。在 Linux 上建议选择「无」。",
|
||||
|
|
|
|||
|
|
@ -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<SettingsStore>()(
|
||||
|
|
|
|||
Loading…
Reference in a new issue