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 414062f..11e4d7e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -180,6 +180,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::models::get_available_models, commands::models::get_model_info, 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/components/settings/LanguageSelector.tsx b/src/components/settings/LanguageSelector.tsx new file mode 100644 index 0000000..5619322 --- /dev/null +++ b/src/components/settings/LanguageSelector.tsx @@ -0,0 +1,217 @@ +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-detect" }, + { code: "ar", name: "Arabic" }, + { code: "bg", name: "Bulgarian" }, + { code: "zh", name: "Chinese" }, + { code: "hr", name: "Croatian" }, + { code: "cs", name: "Czech" }, + { code: "da", name: "Danish" }, + { code: "nl", name: "Dutch" }, + { code: "en", name: "English" }, + { code: "et", name: "Estonian" }, + { code: "fi", name: "Finnish" }, + { code: "fr", name: "French" }, + { code: "de", name: "German" }, + { code: "el", name: "Greek" }, + { code: "he", name: "Hebrew" }, + { code: "hi", name: "Hindi" }, + { code: "hu", name: "Hungarian" }, + { code: "is", name: "Icelandic" }, + { code: "id", name: "Indonesian" }, + { code: "it", name: "Italian" }, + { code: "ja", name: "Japanese" }, + { code: "ko", name: "Korean" }, + { code: "lv", name: "Latvian" }, + { code: "lt", name: "Lithuanian" }, + { code: "no", name: "Norwegian" }, + { code: "fa", name: "Persian" }, + { code: "pl", name: "Polish" }, + { code: "pt", name: "Portuguese" }, + { code: "ro", name: "Romanian" }, + { code: "ru", name: "Russian" }, + { code: "sr", name: "Serbian" }, + { code: "sk", name: "Slovak" }, + { code: "sl", name: "Slovenian" }, + { code: "es", name: "Spanish" }, + { code: "sv", name: "Swedish" }, + { code: "th", name: "Thai" }, + { code: "tr", name: "Turkish" }, + { code: "uk", name: "Ukrainian" }, + { code: "vi", name: "Vietnamese" }, +]; + +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-detect"; + + 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") && ( +
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 9d31e16..b1a7283 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 = () => { @@ -20,6 +21,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({