diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 1cddaa5..6db52a5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,9 +2,19 @@ pub mod audio; pub mod models; use crate::utils::cancel_current_operation; -use tauri::AppHandle; +use tauri::{AppHandle, Manager}; #[tauri::command] pub fn cancel_operation(app: AppHandle) { cancel_current_operation(&app); } + +#[tauri::command] +pub fn get_app_dir_path(app: AppHandle) -> Result { + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data directory: {}", e))?; + + Ok(app_data_dir.to_string_lossy().to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1cb6529..8b5c5a7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -158,8 +158,10 @@ pub fn run() { shortcut::change_translate_to_english_setting, shortcut::change_selected_language_setting, shortcut::change_show_overlay_setting, + shortcut::change_debug_mode_setting, trigger_update_check, commands::cancel_operation, + commands::get_app_dir_path, commands::models::get_available_models, commands::models::get_model_info, commands::models::download_model, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 4a2addc..3e3fa6a 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -32,6 +32,8 @@ pub struct AppSettings { pub selected_language: String, #[serde(default = "default_show_overlay")] pub show_overlay: bool, + #[serde(default = "default_debug_mode")] + pub debug_mode: bool, } fn default_model() -> String { @@ -61,6 +63,11 @@ fn default_show_overlay() -> bool { true } +fn default_debug_mode() -> bool { + // Default to false - debug mode should be opt-in + false +} + pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; pub fn get_default_settings() -> AppSettings { @@ -85,6 +92,7 @@ pub fn get_default_settings() -> AppSettings { current_binding: default_shortcut.to_string(), }, ); + // bindings.insert( // "test".to_string(), // ShortcutBinding { @@ -107,6 +115,7 @@ pub fn get_default_settings() -> AppSettings { translate_to_english: false, selected_language: "auto".to_string(), show_overlay: true, + debug_mode: false, } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index e14f840..d2d609b 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -135,6 +135,14 @@ pub fn change_show_overlay_setting(app: AppHandle, enabled: bool) -> Result<(), Ok(()) } +#[tauri::command] +pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.debug_mode = enabled; + 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/Settings.tsx b/src/components/settings/Settings.tsx index b0d0774..53c3ab9 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { MicrophoneSelector } from "./MicrophoneSelector"; import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone"; import { PushToTalk } from "./PushToTalk"; @@ -9,8 +9,37 @@ import { HandyShortcut } from "./HandyShortcut"; import { TranslateToEnglish } from "./TranslateToEnglish"; import { LanguageSelector } from "./LanguageSelector"; import { SettingsGroup } from "../ui/SettingsGroup"; +import { DebugSettings } from "./debug"; +import { useSettings } from "../../hooks/useSettings"; export const Settings: React.FC = () => { + const { settings, updateSetting } = useSettings(); + + // Handle keyboard shortcuts for debug mode toggle + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + // Check for Ctrl+Shift+D (Windows/Linux) or Cmd+Shift+D (macOS) + const isDebugShortcut = + event.shiftKey && + event.key.toLowerCase() === "d" && + (event.ctrlKey || event.metaKey); + + if (isDebugShortcut) { + event.preventDefault(); + const currentDebugMode = settings?.debug_mode ?? false; + updateSetting("debug_mode", !currentDebugMode); + } + }; + + // Add event listener when component mounts + document.addEventListener("keydown", handleKeyDown); + + // Cleanup event listener when component unmounts + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [settings?.debug_mode, updateSetting]); + return (
@@ -27,6 +56,12 @@ export const Settings: React.FC = () => { + + {settings?.debug_mode && ( + + + + )}
); }; diff --git a/src/components/settings/debug/DebugPaths.tsx b/src/components/settings/debug/DebugPaths.tsx new file mode 100644 index 0000000..094db3f --- /dev/null +++ b/src/components/settings/debug/DebugPaths.tsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { TextDisplay, SettingsGroup } from "../../ui"; + +interface DebugPathsProps { + descriptionMode?: "tooltip" | "inline"; + grouped?: boolean; +} + +export const DebugPaths: React.FC = ({ + descriptionMode = "inline", + grouped = false, +}) => { + const [appDirPath, setAppDirPath] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadPaths = async () => { + try { + const result = await invoke("get_app_dir_path"); + setAppDirPath(result); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load paths"); + } finally { + setLoading(false); + } + }; + + loadPaths(); + }, []); + + const handleCopy = (value: string) => { + // Could add a toast notification here if desired + console.log("Copied to clipboard:", value); + }; + + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+

Error loading paths: {error}

+
+ ); + } + + return ( + + ); +}; diff --git a/src/components/settings/debug/DebugSettings.tsx b/src/components/settings/debug/DebugSettings.tsx new file mode 100644 index 0000000..5688afc --- /dev/null +++ b/src/components/settings/debug/DebugSettings.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { DebugPaths } from "./DebugPaths"; + +interface DebugSettingsProps { + descriptionMode?: "tooltip" | "inline"; + grouped?: boolean; +} + +export const DebugSettings: React.FC = ({ + descriptionMode = "inline", + grouped = false, +}) => { + return ( +
+ {descriptionMode === "inline" && !grouped && ( +
+
+

Debug

+
+

+ Debug information and developer tools. These settings are only + visible when debug mode is enabled. +

+
+ )} + + +
+ ); +}; diff --git a/src/components/settings/debug/index.ts b/src/components/settings/debug/index.ts new file mode 100644 index 0000000..db08089 --- /dev/null +++ b/src/components/settings/debug/index.ts @@ -0,0 +1,2 @@ +export { DebugSettings } from "./DebugSettings"; +export { DebugPaths } from "./DebugPaths"; diff --git a/src/components/ui/SettingContainer.tsx b/src/components/ui/SettingContainer.tsx index d3d7bb1..01d06d5 100644 --- a/src/components/ui/SettingContainer.tsx +++ b/src/components/ui/SettingContainer.tsx @@ -6,6 +6,7 @@ interface SettingContainerProps { children: React.ReactNode; descriptionMode?: "inline" | "tooltip"; grouped?: boolean; + layout?: "horizontal" | "stacked"; } export const SettingContainer: React.FC = ({ @@ -14,6 +15,7 @@ export const SettingContainer: React.FC = ({ children, descriptionMode = "tooltip", grouped = false, + layout = "horizontal", }) => { const [showTooltip, setShowTooltip] = useState(false); const tooltipRef = useRef(null); @@ -41,12 +43,78 @@ export const SettingContainer: React.FC = ({ }; const containerClasses = grouped + ? "px-4 p-2" + : "px-4 p-2 rounded-lg border border-mid-gray/20"; + + if (layout === "stacked") { + if (descriptionMode === "tooltip") { + return ( +
+
+

{title}

+
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + onClick={toggleTooltip} + > + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleTooltip(); + } + }} + > + + + {showTooltip && ( +
+

+ {description} +

+
+
+ )} +
+
+
{children}
+
+ ); + } + + return ( +
+
+

{title}

+

{description}

+
+
{children}
+
+ ); + } + + // Horizontal layout (default) + const horizontalContainerClasses = grouped ? "flex items-center justify-between px-4 p-2" : "flex items-center justify-between px-4 p-2 rounded-lg border border-mid-gray/20"; if (descriptionMode === "tooltip") { return ( -
+

{title}

@@ -96,7 +164,7 @@ export const SettingContainer: React.FC = ({ } return ( -
+

{title}

{description}

diff --git a/src/components/ui/TextDisplay.tsx b/src/components/ui/TextDisplay.tsx new file mode 100644 index 0000000..432294e --- /dev/null +++ b/src/components/ui/TextDisplay.tsx @@ -0,0 +1,93 @@ +import React, { useState } from "react"; +import { SettingContainer } from "./SettingContainer"; + +interface TextDisplayProps { + label: string; + description: string; + value: string; + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; + placeholder?: string; + copyable?: boolean; + monospace?: boolean; + onCopy?: (value: string) => void; +} + +export const TextDisplay: React.FC = ({ + label, + description, + value, + descriptionMode = "tooltip", + grouped = false, + placeholder = "Not available", + copyable = false, + monospace = false, + onCopy, +}) => { + const [showCopied, setShowCopied] = useState(false); + + const handleCopy = async () => { + if (!value || !copyable) return; + + try { + await navigator.clipboard.writeText(value); + setShowCopied(true); + setTimeout(() => setShowCopied(false), 1500); + if (onCopy) { + onCopy(value); + } + } catch (err) { + console.error("Failed to copy to clipboard:", err); + } + }; + + const displayValue = value || placeholder; + const textClasses = monospace ? "font-mono break-all" : "break-words"; + + return ( + +
+
+
+ {displayValue} +
+
+ {copyable && value && ( + + )} +
+
+ ); +}; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 4abd84c..4216f04 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -2,3 +2,4 @@ export { Dropdown } from "./Dropdown"; export { ToggleSwitch } from "./ToggleSwitch"; export { SettingContainer } from "./SettingContainer"; export { SettingsGroup } from "./SettingsGroup"; +export { TextDisplay } from "./TextDisplay"; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 2269919..2223ba8 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -202,6 +202,9 @@ export const useSettings = (): UseSettingsReturn => { case "show_overlay": await invoke("change_show_overlay_setting", { enabled: value }); break; + case "debug_mode": + await invoke("change_debug_mode_setting", { enabled: value }); + break; case "bindings": // Handle bindings separately - they use their own invoke methods break; @@ -247,6 +250,7 @@ export const useSettings = (): UseSettingsReturn => { translate_to_english: false, selected_language: "auto", show_overlay: true, + debug_mode: false, }; const defaultValue = defaults[key]; diff --git a/src/lib/types.ts b/src/lib/types.ts index ba3c49f..a122a4b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -30,6 +30,7 @@ export const SettingsSchema = z.object({ translate_to_english: z.boolean(), selected_language: z.string(), show_overlay: z.boolean(), + debug_mode: z.boolean(), }); export const BindingResponseSchema = z.object({