diff --git a/package.json b/package.json index 0740615..2c43f39 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "handy-app", "private": true, - "version": "0.3.1", + "version": "0.3.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8c86030..2ca528c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2003,7 +2003,7 @@ dependencies = [ [[package]] name = "handy" -version = "0.3.1" +version = "0.3.2" dependencies = [ "anyhow", "cpal", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5e5ac04..79304c9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "handy" -version = "0.3.1" +version = "0.3.2" description = "Handy" authors = ["cjpais"] edition = "2021" diff --git a/src-tauri/resources/default_settings.json b/src-tauri/resources/default_settings.json index a41e7c5..c7ec6b4 100644 --- a/src-tauri/resources/default_settings.json +++ b/src-tauri/resources/default_settings.json @@ -7,5 +7,6 @@ "default_binding": "Platform-specific: ctrl+space (Windows/Linux), alt+space (macOS)" } }, - "push_to_talk": true + "push_to_talk": true, + "selected_language": "auto" } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 39879f7..eb8e6fe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,28 @@ struct ShortcutToggleStates { type ManagedToggleState = Mutex; +fn show_main_window(app: &AppHandle) { + if let Some(main_window) = app.get_webview_window("main") { + // First, ensure the window is visible + if let Err(e) = main_window.show() { + eprintln!("Failed to show window: {}", e); + } + // Then, bring it to the front and give it focus + if let Err(e) = main_window.set_focus() { + eprintln!("Failed to focus window: {}", e); + } + // Optional: On macOS, ensure the app becomes active if it was an accessory + #[cfg(target_os = "macos")] + { + if let Err(e) = app.set_activation_policy(tauri::ActivationPolicy::Regular) { + eprintln!("Failed to set activation policy to Regular: {}", e); + } + } + } else { + eprintln!("Main window not found"); + } +} + #[tauri::command] fn trigger_update_check(app: AppHandle) -> Result<(), String> { app.emit("check-for-updates", ()) @@ -58,31 +80,13 @@ pub fn run() { tauri::path::BaseDirectory::Resource, )?)?) .show_menu_on_left_click(true) + .icon_as_template(true) .on_menu_event(|app, event| match event.id.as_ref() { "settings" => { - if let Some(settings_window) = app.get_webview_window("main") { - // First, ensure the window is visible - if let Err(e) = settings_window.show() { - eprintln!("Failed to show window: {}", e); - } - // Then, bring it to the front and give it focus - if let Err(e) = settings_window.set_focus() { - eprintln!("Failed to focus window: {}", e); - } - // Optional: On macOS, ensure the app becomes active if it was an accessory - #[cfg(target_os = "macos")] - { - if let Err(e) = - app.set_activation_policy(tauri::ActivationPolicy::Regular) - { - eprintln!("Failed to set activation policy to Regular: {}", e); - } - } - } else { - eprintln!("Main window not found"); - } + show_main_window(app); } "check_updates" => { + show_main_window(app); let _ = app.emit("check-for-updates", ()); } "cancel" => { @@ -148,6 +152,7 @@ pub fn run() { shortcut::change_ptt_setting, shortcut::change_audio_feedback_setting, shortcut::change_translate_to_english_setting, + shortcut::change_selected_language_setting, trigger_update_check, commands::cancel_operation, commands::models::get_available_models, diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index 5212f7e..bfd3e79 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -178,7 +178,8 @@ impl TranscriptionManager { // Initialize parameters let mut params = FullParams::new(SamplingStrategy::default()); - params.set_language(Some("auto")); + let language = Some(settings.selected_language.as_str()); + params.set_language(language); params.set_print_special(false); params.set_print_progress(false); params.set_print_realtime(false); diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 7b441ee..f46ff59 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -28,6 +28,8 @@ pub struct AppSettings { pub selected_output_device: Option, #[serde(default = "default_translate_to_english")] pub translate_to_english: bool, + #[serde(default = "default_selected_language")] + pub selected_language: String, } fn default_model() -> String { @@ -47,6 +49,11 @@ fn default_translate_to_english() -> bool { false } +fn default_selected_language() -> String { + // Default to auto-detection for backward compatibility + "auto".to_string() +} + pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; pub fn get_default_settings() -> AppSettings { @@ -91,6 +98,7 @@ pub fn get_default_settings() -> AppSettings { selected_microphone: None, selected_output_device: None, translate_to_english: false, + selected_language: "auto".to_string(), } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 516a32e..82d54b6 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -119,6 +119,14 @@ pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Res Ok(()) } +#[tauri::command] +pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.selected_language = language; + settings::write_settings(&app, settings); + Ok(()) +} + fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { // Parse shortcut and return error if it fails let shortcut = match binding.current_binding.parse::() { diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 3e6e0a8..400b271 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -209,6 +209,7 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState) { let tray = app.state::(); let _ = tray.set_menu(Some(menu)); + let _ = tray.set_icon_as_template(true); } /// Plays an audio resource from the resources directory. diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 41a7d64..5a0f2eb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Handy", - "version": "0.3.1", + "version": "0.3.2", "identifier": "com.pais.handy", "build": { "beforeDevCommand": "bun run dev", @@ -14,9 +14,9 @@ { "title": "Handy", "width": 540, - "height": 630, + "height": 680, "minWidth": 540, - "minHeight": 630, + "minHeight": 680, "resizable": true, "maximizable": false } diff --git a/src/components/settings/LanguageSelector.tsx b/src/components/settings/LanguageSelector.tsx new file mode 100644 index 0000000..c29db8c --- /dev/null +++ b/src/components/settings/LanguageSelector.tsx @@ -0,0 +1,218 @@ +import React, { useState, useRef, useEffect } from "react"; +import { SettingContainer } from "../ui/SettingContainer"; +import ResetIcon from "../icons/ResetIcon"; +import { useSettings } from "../../hooks/useSettings"; + +interface LanguageSelectorProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +const LANGUAGES = [ + { code: "auto", name: "Auto" }, + { code: "en", name: "English" }, + { code: "zh", name: "Chinese" }, + { code: "hi", name: "Hindi" }, + { code: "es", name: "Spanish" }, + { code: "fr", name: "French" }, + { code: "ar", name: "Arabic" }, + { code: "pt", name: "Portuguese" }, + { code: "ru", name: "Russian" }, + { code: "ja", name: "Japanese" }, + { code: "de", name: "German" }, + { code: "ko", name: "Korean" }, + { code: "it", name: "Italian" }, + { code: "tr", name: "Turkish" }, + { code: "vi", name: "Vietnamese" }, + { code: "pl", name: "Polish" }, + { code: "nl", name: "Dutch" }, + { code: "uk", name: "Ukrainian" }, + { code: "fa", name: "Persian" }, + { code: "th", name: "Thai" }, + { code: "ro", name: "Romanian" }, + { code: "el", name: "Greek" }, + { code: "cs", name: "Czech" }, + { code: "sv", name: "Swedish" }, + { code: "hu", name: "Hungarian" }, + { code: "he", name: "Hebrew" }, + { code: "da", name: "Danish" }, + { code: "fi", name: "Finnish" }, + { code: "no", name: "Norwegian" }, + { code: "sk", name: "Slovak" }, + { code: "bg", name: "Bulgarian" }, + { code: "hr", name: "Croatian" }, + { code: "lt", name: "Lithuanian" }, + { code: "sl", name: "Slovenian" }, + { code: "lv", name: "Latvian" }, + { code: "et", name: "Estonian" }, + { code: "sr", name: "Serbian" }, + { code: "is", name: "Icelandic" }, + { code: "id", name: "Indonesian" }, +]; + +export const LanguageSelector: React.FC = ({ + descriptionMode = "tooltip", + grouped = false, +}) => { + const { getSetting, updateSetting, resetSetting, isUpdating } = useSettings(); + const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const dropdownRef = useRef(null); + const searchInputRef = useRef(null); + + const selectedLanguage = getSetting("selected_language") || "auto"; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + setSearchQuery(""); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + useEffect(() => { + if (isOpen && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, [isOpen]); + + const filteredLanguages = LANGUAGES.filter((language) => + language.name.toLowerCase().includes(searchQuery.toLowerCase()), + ); + + const selectedLanguageName = + LANGUAGES.find((lang) => lang.code === selectedLanguage)?.name || "Auto"; + + const handleLanguageSelect = async (languageCode: string) => { + await updateSetting("selected_language", languageCode); + setIsOpen(false); + setSearchQuery(""); + }; + + const handleReset = async () => { + await resetSetting("selected_language"); + }; + + const handleToggle = () => { + if (isUpdating("selected_language")) return; + setIsOpen(!isOpen); + }; + + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && filteredLanguages.length > 0) { + // Select first filtered language on Enter + handleLanguageSelect(filteredLanguages[0].code); + } else if (event.key === "Escape") { + setIsOpen(false); + setSearchQuery(""); + } + }; + + return ( + +
+
+ + + {isOpen && !isUpdating("selected_language") && ( +
+ {/* Search input */} +
+ +
+ +
+ {filteredLanguages.length === 0 ? ( +
+ No languages found +
+ ) : ( + filteredLanguages.map((language) => ( + + )) + )} +
+
+ )} +
+ +
+ {isUpdating("selected_language") && ( +
+
+
+ )} +
+ ); +}; diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 9d31e16..b2a33fa 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -6,6 +6,7 @@ import { AudioFeedback } from "./AudioFeedback"; import { OutputDeviceSelector } from "./OutputDeviceSelector"; import { HandyShortcut } from "./HandyShortcut"; import { TranslateToEnglish } from "./TranslateToEnglish"; +import { LanguageSelector } from "./LanguageSelector"; import { SettingsGroup } from "../ui/SettingsGroup"; export const Settings: React.FC = () => { @@ -14,6 +15,7 @@ export const Settings: React.FC = () => { + diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index e1170ae..7931b33 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -194,6 +194,11 @@ export const useSettings = (): UseSettingsReturn => { enabled: value, }); break; + case "selected_language": + await invoke("change_selected_language_setting", { + language: value, + }); + break; case "bindings": // Handle bindings separately - they use their own invoke methods break; @@ -237,6 +242,7 @@ export const useSettings = (): UseSettingsReturn => { selected_microphone: "Default", selected_output_device: "Default", translate_to_english: false, + selected_language: "auto", }; const defaultValue = defaults[key]; diff --git a/src/lib/types.ts b/src/lib/types.ts index e8314c1..ff90867 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -28,6 +28,7 @@ export const SettingsSchema = z.object({ selected_microphone: z.string().nullable().optional(), selected_output_device: z.string().nullable().optional(), translate_to_english: z.boolean(), + selected_language: z.string(), }); export const BindingResponseSchema = z.object({