add a very simple debug mode toggle
This commit is contained in:
parent
87469771c0
commit
1ce954d02f
13 changed files with 336 additions and 4 deletions
|
|
@ -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<String, String> {
|
||||
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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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::<Shortcut>() {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||
<SettingsGroup>
|
||||
|
|
@ -27,6 +56,12 @@ export const Settings: React.FC = () => {
|
|||
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
|
||||
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
||||
</SettingsGroup>
|
||||
|
||||
{settings?.debug_mode && (
|
||||
<SettingsGroup title="Debug">
|
||||
<DebugSettings descriptionMode="tooltip" grouped={true} />
|
||||
</SettingsGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
69
src/components/settings/debug/DebugPaths.tsx
Normal file
69
src/components/settings/debug/DebugPaths.tsx
Normal file
|
|
@ -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<DebugPathsProps> = ({
|
||||
descriptionMode = "inline",
|
||||
grouped = false,
|
||||
}) => {
|
||||
const [appDirPath, setAppDirPath] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPaths = async () => {
|
||||
try {
|
||||
const result = await invoke<string>("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 (
|
||||
<div className="space-y-4">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2"></div>
|
||||
<div className="h-8 bg-gray-100 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-600 text-sm">Error loading paths: {error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextDisplay
|
||||
label="App Data Directory"
|
||||
description="Main directory where application data, settings, and models are stored"
|
||||
value={appDirPath}
|
||||
descriptionMode={descriptionMode}
|
||||
grouped={true}
|
||||
copyable={true}
|
||||
monospace={true}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
30
src/components/settings/debug/DebugSettings.tsx
Normal file
30
src/components/settings/debug/DebugSettings.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
import { DebugPaths } from "./DebugPaths";
|
||||
|
||||
interface DebugSettingsProps {
|
||||
descriptionMode?: "tooltip" | "inline";
|
||||
grouped?: boolean;
|
||||
}
|
||||
|
||||
export const DebugSettings: React.FC<DebugSettingsProps> = ({
|
||||
descriptionMode = "inline",
|
||||
grouped = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{descriptionMode === "inline" && !grouped && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Debug</h2>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Debug information and developer tools. These settings are only
|
||||
visible when debug mode is enabled.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DebugPaths descriptionMode={descriptionMode} grouped={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
src/components/settings/debug/index.ts
Normal file
2
src/components/settings/debug/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { DebugSettings } from "./DebugSettings";
|
||||
export { DebugPaths } from "./DebugPaths";
|
||||
|
|
@ -6,6 +6,7 @@ interface SettingContainerProps {
|
|||
children: React.ReactNode;
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
grouped?: boolean;
|
||||
layout?: "horizontal" | "stacked";
|
||||
}
|
||||
|
||||
export const SettingContainer: React.FC<SettingContainerProps> = ({
|
||||
|
|
@ -14,6 +15,7 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
|
|||
children,
|
||||
descriptionMode = "tooltip",
|
||||
grouped = false,
|
||||
layout = "horizontal",
|
||||
}) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -41,12 +43,78 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
|
|||
};
|
||||
|
||||
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 (
|
||||
<div className={containerClasses}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className="relative"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onClick={toggleTooltip}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-mid-gray cursor-help hover:text-logo-primary transition-colors duration-200 select-none"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="More information"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggleTooltip();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{showTooltip && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-background border border-mid-gray/80 rounded-lg shadow-lg z-50 max-w-xs min-w-[200px] whitespace-normal animate-in fade-in-0 zoom-in-95 duration-200">
|
||||
<p className="text-sm text-center leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-mid-gray/80"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className="mb-2">
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
<p className="text-sm">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className={containerClasses}>
|
||||
<div className={horizontalContainerClasses}>
|
||||
<div className="max-w-2/3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium ">{title}</h3>
|
||||
|
|
@ -96,7 +164,7 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={horizontalContainerClasses}>
|
||||
<div className="max-w-2/3">
|
||||
<h3 className="text-sm font-medium ">{title}</h3>
|
||||
<p className="text-sm">{description}</p>
|
||||
|
|
|
|||
93
src/components/ui/TextDisplay.tsx
Normal file
93
src/components/ui/TextDisplay.tsx
Normal file
|
|
@ -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<TextDisplayProps> = ({
|
||||
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 (
|
||||
<SettingContainer
|
||||
title={label}
|
||||
description={description}
|
||||
descriptionMode={descriptionMode}
|
||||
grouped={grouped}
|
||||
layout="stacked"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className={`px-2 min-h-8 flex items-center bg-mid-gray/10 border border-mid-gray/80 rounded text-xs ${textClasses} ${!value ? "opacity-60" : ""}`}
|
||||
>
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
{copyable && value && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center justify-center px-2 py-1 w-12 min-h-8 text-xs font-semibold bg-mid-gray/10 hover:bg-logo-primary/10 border border-mid-gray/80 hover:border-logo-primary hover:text-logo-primary rounded transition-all duration-150 flex-shrink-0 cursor-pointer"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{showCopied ? (
|
||||
<div className="flex items-center space-x-1">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
"Copy"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</SettingContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,3 +2,4 @@ export { Dropdown } from "./Dropdown";
|
|||
export { ToggleSwitch } from "./ToggleSwitch";
|
||||
export { SettingContainer } from "./SettingContainer";
|
||||
export { SettingsGroup } from "./SettingsGroup";
|
||||
export { TextDisplay } from "./TextDisplay";
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue