diff --git a/src/components/settings/AlwaysOnMicrophone.tsx b/src/components/settings/AlwaysOnMicrophone.tsx index 1acabb6..c4c40c1 100644 --- a/src/components/settings/AlwaysOnMicrophone.tsx +++ b/src/components/settings/AlwaysOnMicrophone.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; +import React from "react"; import { ToggleSwitch } from "../ui/ToggleSwitch"; +import { useSettings } from "../../hooks/useSettings"; interface AlwaysOnMicrophoneProps { descriptionMode?: "inline" | "tooltip"; @@ -11,46 +11,15 @@ export const AlwaysOnMicrophone: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { - const [alwaysOnMode, setAlwaysOnMode] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); + const { getSetting, updateSetting, isUpdating } = useSettings(); - useEffect(() => { - loadSettings(); - }, []); - - const loadSettings = async () => { - try { - const alwaysOn: boolean = await invoke("get_microphone_mode"); - setAlwaysOnMode(alwaysOn); - } catch (error) { - console.error("Failed to load always-on microphone setting:", error); - } - }; - - const handleAlwaysOnToggle = async (enabled: boolean) => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("update_microphone_mode", { alwaysOn: enabled }); - setAlwaysOnMode(enabled); - - // Provide user feedback about the change - console.log(`Always-on microphone ${enabled ? "enabled" : "disabled"}`); - } catch (error) { - console.error("Failed to update microphone mode:", error); - // Revert the toggle if the update failed - setAlwaysOnMode(!enabled); - } finally { - setIsUpdating(false); - } - }; + const alwaysOnMode = getSetting("always_on_microphone") || false; return ( updateSetting("always_on_microphone", enabled)} + isUpdating={isUpdating("always_on_microphone")} label="Always-On Microphone" description="Keep microphone active for low latency recording. This may prevent your computer from sleeping." descriptionMode={descriptionMode} diff --git a/src/components/settings/AudioFeedback.tsx b/src/components/settings/AudioFeedback.tsx index de768ce..dfe73e5 100644 --- a/src/components/settings/AudioFeedback.tsx +++ b/src/components/settings/AudioFeedback.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; +import React from "react"; import { ToggleSwitch } from "../ui/ToggleSwitch"; +import { useSettings } from "../../hooks/useSettings"; interface AudioFeedbackProps { descriptionMode?: "inline" | "tooltip"; @@ -11,55 +11,15 @@ export const AudioFeedback: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { - const [audioFeedbackEnabled, setAudioFeedbackEnabled] = - useState(false); - const [isUpdating, setIsUpdating] = useState(false); + const { getSetting, updateSetting, isUpdating } = useSettings(); - useEffect(() => { - loadSettings(); - }, []); - - const loadSettings = async () => { - try { - // Load from the store since this setting is handled differently - const { load } = await import("@tauri-apps/plugin-store"); - const store = await load("settings_store.json", { autoSave: false }); - const settings = await store.get("settings"); - - if ( - settings && - typeof settings === "object" && - "audio_feedback" in settings - ) { - setAudioFeedbackEnabled(settings.audio_feedback as boolean); - } - } catch (error) { - console.error("Failed to load audio feedback setting:", error); - } - }; - - const handleAudioFeedbackToggle = async (enabled: boolean) => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("change_audio_feedback_setting", { enabled }); - setAudioFeedbackEnabled(enabled); - console.log(`Audio feedback ${enabled ? "enabled" : "disabled"}`); - } catch (error) { - console.error("Failed to update audio feedback setting:", error); - // Revert the toggle if the update failed - setAudioFeedbackEnabled(!enabled); - } finally { - setIsUpdating(false); - } - }; + const audioFeedbackEnabled = getSetting("audio_feedback") || false; return ( updateSetting("audio_feedback", enabled)} + isUpdating={isUpdating("audio_feedback")} label="Audio Feedback" description="Play sound when recording starts and stops" descriptionMode={descriptionMode} diff --git a/src/components/settings/HandyShortcut.tsx b/src/components/settings/HandyShortcut.tsx index 6bae54e..6ea5459 100644 --- a/src/components/settings/HandyShortcut.tsx +++ b/src/components/settings/HandyShortcut.tsx @@ -1,14 +1,10 @@ import React, { useEffect, useState, useRef } from "react"; -import { - BindingResponseSchema, - SettingsSchema, - ShortcutBindingsMap, -} from "../../lib/types"; -import { invoke } from "@tauri-apps/api/core"; +import { BindingResponseSchema, ShortcutBindingsMap } from "../../lib/types"; import { type } from "@tauri-apps/plugin-os"; import { getKeyName } from "../../lib/utils/keyboard"; import ResetIcon from "../icons/ResetIcon"; import { SettingContainer } from "../ui/SettingContainer"; +import { useSettings } from "../../hooks/useSettings"; interface HandyShortcutProps { descriptionMode?: "inline" | "tooltip"; @@ -19,7 +15,8 @@ export const HandyShortcut: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { - const [bindings, setBindings] = React.useState({}); + const { getSetting, updateBinding, resetBinding, isUpdating, isLoading } = + useSettings(); const [keyPressed, setKeyPressed] = useState([]); const [recordedKeys, setRecordedKeys] = useState([]); const [editingShortcutId, setEditingShortcutId] = useState( @@ -27,9 +24,10 @@ export const HandyShortcut: React.FC = ({ ); const [originalBinding, setOriginalBinding] = useState(""); const [isMacOS, setIsMacOS] = useState(false); - const [isLoading, setIsLoading] = useState(true); const shortcutRefs = useRef>(new Map()); + const bindings = getSetting("bindings") || {}; + // Check if running on macOS useEffect(() => { const checkOsType = async () => { @@ -76,28 +74,6 @@ export const HandyShortcut: React.FC = ({ return combination.split("+").map(formatMacOSKeys).join(" + "); }; - useEffect(() => { - loadSettings(); - }, []); - - const loadSettings = async () => { - try { - setIsLoading(true); - const { load } = await import("@tauri-apps/plugin-store"); - const store = await load("settings_store.json", { autoSave: false }); - const settings = await store.get("settings"); - - if (settings) { - const parsedSettings = SettingsSchema.parse(settings); - setBindings(parsedSettings.bindings); - } - } catch (error) { - console.error("Failed to load settings:", error); - } finally { - setIsLoading(false); - } - }; - useEffect(() => { // Only add event listeners when we're in editing mode if (editingShortcutId === null) return; @@ -140,31 +116,10 @@ export const HandyShortcut: React.FC = ({ const newShortcut = recordedKeys.join("+"); if (editingShortcutId && bindings[editingShortcutId]) { - const updatedBinding = { - ...bindings[editingShortcutId], - current_binding: newShortcut, - }; - - setBindings((prev) => ({ - ...prev, - [editingShortcutId]: updatedBinding, - })); - try { - await invoke("change_binding", { - id: editingShortcutId, - binding: newShortcut, - }); + await updateBinding(editingShortcutId, newShortcut); } catch (error) { console.error("Failed to change binding:", error); - // Restore original binding on error - setBindings((prev) => ({ - ...prev, - [editingShortcutId]: { - ...prev[editingShortcutId], - current_binding: originalBinding, - }, - })); } // Exit editing mode and reset states @@ -180,22 +135,11 @@ export const HandyShortcut: React.FC = ({ const handleClickOutside = (e: MouseEvent) => { const activeElement = shortcutRefs.current.get(editingShortcutId); if (activeElement && !activeElement.contains(e.target as Node)) { - // Cancel shortcut recording and restore original value - if (editingShortcutId && bindings[editingShortcutId]) { - setBindings((prev) => ({ - ...prev, - [editingShortcutId]: { - ...prev[editingShortcutId], - current_binding: originalBinding, - }, - })); - - // Reset states - setEditingShortcutId(null); - setKeyPressed([]); - setRecordedKeys([]); - setOriginalBinding(""); - } + // Cancel shortcut recording - the hook will handle rollback + setEditingShortcutId(null); + setKeyPressed([]); + setRecordedKeys([]); + setOriginalBinding(""); } }; @@ -208,7 +152,14 @@ export const HandyShortcut: React.FC = ({ window.removeEventListener("keyup", handleKeyUp); window.removeEventListener("click", handleClickOutside); }; - }, [keyPressed, recordedKeys, editingShortcutId, bindings, originalBinding]); + }, [ + keyPressed, + recordedKeys, + editingShortcutId, + bindings, + originalBinding, + updateBinding, + ]); // Start recording a new shortcut const startRecording = (id: string) => { @@ -302,24 +253,8 @@ export const HandyShortcut: React.FC = ({ )} diff --git a/src/components/settings/MicrophoneSelector.tsx b/src/components/settings/MicrophoneSelector.tsx index 7a36510..1ddf5eb 100644 --- a/src/components/settings/MicrophoneSelector.tsx +++ b/src/components/settings/MicrophoneSelector.tsx @@ -1,9 +1,26 @@ -import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import { AudioDevice } from "../../lib/types"; +import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import ResetIcon from "../icons/ResetIcon"; +import { useSettings } from "../../hooks/useSettings"; + +// Simple refresh icon component +const RefreshIcon: React.FC<{ className?: string }> = ({ className = "" }) => ( + + + +); interface MicrophoneSelectorProps { descriptionMode?: "inline" | "tooltip"; @@ -14,73 +31,28 @@ export const MicrophoneSelector: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { - const [audioDevices, setAudioDevices] = useState([]); - const [selectedMicrophone, setSelectedMicrophone] = - useState("default"); - const [isLoading, setIsLoading] = useState(true); - const [isUpdating, setIsUpdating] = useState(false); + const { + getSetting, + updateSetting, + resetSetting, + isUpdating, + isLoading, + audioDevices, + refreshAudioDevices, + } = useSettings(); - useEffect(() => { - loadAudioDevices(); - loadSettings(); - }, []); - - const loadAudioDevices = async () => { - try { - const devices: AudioDevice[] = await invoke("get_available_microphones"); - setAudioDevices(devices); - } catch (error) { - console.error("Failed to load audio devices:", error); - setAudioDevices([]); - } - }; - - const loadSettings = async () => { - try { - setIsLoading(true); - const selectedMic: string = await invoke("get_selected_microphone"); - setSelectedMicrophone(selectedMic); - } catch (error) { - console.error("Failed to load microphone settings:", error); - } finally { - setIsLoading(false); - } - }; + const selectedMicrophone = getSetting("selected_microphone") || "Default"; const handleMicrophoneSelect = async (deviceName: string) => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("set_selected_microphone", { deviceName }); - setSelectedMicrophone(deviceName); - console.log( - `Microphone changed to: ${audioDevices.find((d) => d.name === deviceName)?.name}`, - ); - } catch (error) { - console.error("Failed to set microphone:", error); - // Revert selection if update failed - loadSettings(); - } finally { - setIsUpdating(false); - } + await updateSetting("selected_microphone", deviceName); + console.log( + `Microphone changed to: ${audioDevices.find((d) => d.name === deviceName)?.name}`, + ); }; const handleReset = async () => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("set_selected_microphone", { deviceName: "default" }); - setSelectedMicrophone("default"); - console.log("Microphone reset to default"); - } catch (error) { - console.error("Failed to reset microphone:", error); - // Revert selection if reset failed - loadSettings(); - } finally { - setIsUpdating(false); - } + await resetSetting("selected_microphone"); + console.log("Microphone reset to default"); }; return ( @@ -96,17 +68,18 @@ export const MicrophoneSelector: React.FC = ({ selectedDevice={selectedMicrophone} onSelect={handleMicrophoneSelect} placeholder={isLoading ? "Loading..." : "Select microphone..."} - disabled={isUpdating || isLoading} + disabled={isUpdating("selected_microphone") || isLoading} + refreshDevices={refreshAudioDevices} /> - {isUpdating && ( + {isUpdating("selected_microphone") && (
diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index e66c6db..12f4cfe 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import { AudioDevice } from "../../lib/types"; +import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import ResetIcon from "../icons/ResetIcon"; +import { useSettings } from "../../hooks/useSettings"; interface OutputDeviceSelectorProps { descriptionMode?: "inline" | "tooltip"; @@ -14,75 +13,29 @@ export const OutputDeviceSelector: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { - const [outputDevices, setOutputDevices] = useState([]); - const [selectedOutputDevice, setSelectedOutputDevice] = - useState("default"); - const [isLoading, setIsLoading] = useState(true); - const [isUpdating, setIsUpdating] = useState(false); + const { + getSetting, + updateSetting, + resetSetting, + isUpdating, + isLoading, + outputDevices, + refreshOutputDevices, + } = useSettings(); - useEffect(() => { - loadOutputDevices(); - loadSettings(); - }, []); - - const loadOutputDevices = async () => { - try { - const devices: AudioDevice[] = await invoke( - "get_available_output_devices", - ); - setOutputDevices(devices); - } catch (error) { - console.error("Failed to load output devices:", error); - setOutputDevices([]); - } - }; - - const loadSettings = async () => { - try { - setIsLoading(true); - const selectedDevice: string = await invoke("get_selected_output_device"); - setSelectedOutputDevice(selectedDevice); - } catch (error) { - console.error("Failed to load output device settings:", error); - } finally { - setIsLoading(false); - } - }; + const selectedOutputDevice = + getSetting("selected_output_device") || "Default"; const handleOutputDeviceSelect = async (deviceName: string) => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("set_selected_output_device", { deviceName }); - setSelectedOutputDevice(deviceName); - console.log( - `Output device changed to: ${outputDevices.find((d) => d.name === deviceName)?.name}`, - ); - } catch (error) { - console.error("Failed to set output device:", error); - // Revert selection if update failed - loadSettings(); - } finally { - setIsUpdating(false); - } + await updateSetting("selected_output_device", deviceName); + console.log( + `Output device changed to: ${outputDevices.find((d) => d.name === deviceName)?.name}`, + ); }; const handleReset = async () => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("set_selected_output_device", { deviceName: "default" }); - setSelectedOutputDevice("default"); - console.log("Output device reset to default"); - } catch (error) { - console.error("Failed to reset output device:", error); - // Revert selection if reset failed - loadSettings(); - } finally { - setIsUpdating(false); - } + await resetSetting("selected_output_device"); + console.log("Output device reset to default"); }; return ( @@ -98,17 +51,18 @@ export const OutputDeviceSelector: React.FC = ({ selectedDevice={selectedOutputDevice} onSelect={handleOutputDeviceSelect} placeholder={isLoading ? "Loading..." : "Select output device..."} - disabled={isUpdating || isLoading} + disabled={isUpdating("selected_output_device") || isLoading} + refreshDevices={refreshOutputDevices} /> - {isUpdating && ( + {isUpdating("selected_output_device") && (
diff --git a/src/components/settings/PushToTalk.tsx b/src/components/settings/PushToTalk.tsx index 8c04d7a..848e504 100644 --- a/src/components/settings/PushToTalk.tsx +++ b/src/components/settings/PushToTalk.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; +import React from "react"; import { ToggleSwitch } from "../ui/ToggleSwitch"; +import { useSettings } from "../../hooks/useSettings"; interface PushToTalkProps { descriptionMode?: "inline" | "tooltip"; @@ -11,54 +11,15 @@ export const PushToTalk: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { - const [pttEnabled, setPttEnabled] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); + const { getSetting, updateSetting, isUpdating } = useSettings(); - useEffect(() => { - loadSettings(); - }, []); - - const loadSettings = async () => { - try { - // Load from the store since this setting is handled differently - const { load } = await import("@tauri-apps/plugin-store"); - const store = await load("settings_store.json", { autoSave: false }); - const settings = await store.get("settings"); - - if ( - settings && - typeof settings === "object" && - "push_to_talk" in settings - ) { - setPttEnabled(settings.push_to_talk as boolean); - } - } catch (error) { - console.error("Failed to load push-to-talk setting:", error); - } - }; - - const handlePttToggle = async (enabled: boolean) => { - if (isUpdating) return; - - try { - setIsUpdating(true); - await invoke("change_ptt_setting", { enabled }); - setPttEnabled(enabled); - console.log(`Push-to-talk ${enabled ? "enabled" : "disabled"}`); - } catch (error) { - console.error("Failed to update push-to-talk setting:", error); - // Revert the toggle if the update failed - setPttEnabled(!enabled); - } finally { - setIsUpdating(false); - } - }; + const pttEnabled = getSetting("push_to_talk") || false; return ( updateSetting("push_to_talk", enabled)} + isUpdating={isUpdating("push_to_talk")} label="Push To Talk" description="Hold to record, release to stop" descriptionMode={descriptionMode} diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index 2ea9a9f..3997e74 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -7,6 +7,7 @@ interface DropdownProps { onSelect: (deviceName: string) => void; placeholder?: string; disabled?: boolean; + refreshDevices?: () => void; } export const Dropdown: React.FC = ({ @@ -15,6 +16,7 @@ export const Dropdown: React.FC = ({ onSelect, placeholder = "Select a microphone...", disabled = false, + refreshDevices, }) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -36,9 +38,19 @@ export const Dropdown: React.FC = ({ }; }, []); - // Find the selected device name + // Find the selected device name with proper default handling const selectedDeviceName = selectedDevice - ? devices.find((d) => d.name === selectedDevice)?.name || "Unknown Device" + ? (() => { + // First try exact match + let device = devices.find((d) => d.name === selectedDevice); + + // If no exact match and selected is "default" or "Default", try both variants + if (!device && selectedDevice.toLowerCase() === "default") { + device = devices.find((d) => d.name.toLowerCase() === "default"); + } + + return device?.name || "Unknown Device"; + })() : null; const handleSelect = (deviceName: string) => { @@ -46,6 +58,17 @@ export const Dropdown: React.FC = ({ setIsOpen(false); }; + const handleToggle = () => { + if (disabled) return; + + // Refresh devices when opening the dropdown + if (!isOpen && refreshDevices) { + refreshDevices(); + } + + setIsOpen(!isOpen); + }; + return (