387 lines
11 KiB
TypeScript
387 lines
11 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { Settings, AudioDevice } from "../lib/types";
|
|
|
|
interface SettingsState {
|
|
settings: Settings | null;
|
|
isLoading: boolean;
|
|
isUpdating: Record<string, boolean>;
|
|
audioDevices: AudioDevice[];
|
|
outputDevices: AudioDevice[];
|
|
}
|
|
|
|
interface UseSettingsReturn {
|
|
// State
|
|
settings: Settings | null;
|
|
isLoading: boolean;
|
|
isUpdating: (key: string) => boolean;
|
|
audioDevices: AudioDevice[];
|
|
outputDevices: AudioDevice[];
|
|
|
|
// Actions
|
|
updateSetting: <K extends keyof Settings>(
|
|
key: K,
|
|
value: Settings[K],
|
|
) => Promise<void>;
|
|
resetSetting: (key: keyof Settings) => Promise<void>;
|
|
refreshSettings: () => Promise<void>;
|
|
refreshAudioDevices: () => Promise<void>;
|
|
refreshOutputDevices: () => Promise<void>;
|
|
|
|
// Binding-specific actions
|
|
updateBinding: (id: string, binding: string) => Promise<void>;
|
|
resetBinding: (id: string) => Promise<void>;
|
|
|
|
// Convenience getters
|
|
getSetting: <K extends keyof Settings>(key: K) => Settings[K] | undefined;
|
|
}
|
|
|
|
export const useSettings = (): UseSettingsReturn => {
|
|
const [state, setState] = useState<SettingsState>({
|
|
settings: null,
|
|
isLoading: true,
|
|
isUpdating: {},
|
|
audioDevices: [],
|
|
outputDevices: [],
|
|
});
|
|
|
|
// Load settings from store
|
|
const loadSettings = useCallback(async () => {
|
|
try {
|
|
const { load } = await import("@tauri-apps/plugin-store");
|
|
const store = await load("settings_store.json", { autoSave: false });
|
|
const settings = (await store.get("settings")) as Settings;
|
|
|
|
// Load additional settings that come from invoke calls
|
|
const [microphoneMode, selectedMicrophone, selectedOutputDevice] =
|
|
await Promise.allSettled([
|
|
invoke("get_microphone_mode"),
|
|
invoke("get_selected_microphone"),
|
|
invoke("get_selected_output_device"),
|
|
]);
|
|
|
|
// Merge all settings
|
|
const mergedSettings: Settings = {
|
|
...settings,
|
|
always_on_microphone:
|
|
microphoneMode.status === "fulfilled"
|
|
? (microphoneMode.value as boolean)
|
|
: false,
|
|
selected_microphone:
|
|
selectedMicrophone.status === "fulfilled"
|
|
? (selectedMicrophone.value as string)
|
|
: "Default",
|
|
selected_output_device:
|
|
selectedOutputDevice.status === "fulfilled"
|
|
? (selectedOutputDevice.value as string)
|
|
: "Default",
|
|
};
|
|
|
|
setState((prev) => ({
|
|
...prev,
|
|
settings: mergedSettings,
|
|
isLoading: false,
|
|
}));
|
|
} catch (error) {
|
|
console.error("Failed to load settings:", error);
|
|
setState((prev) => ({
|
|
...prev,
|
|
isLoading: false,
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
// Load audio devices
|
|
const loadAudioDevices = useCallback(async () => {
|
|
try {
|
|
const devices: AudioDevice[] = await invoke("get_available_microphones");
|
|
|
|
// Always ensure "Default" is available as the first option
|
|
const devicesWithDefault = [
|
|
{ index: "default", name: "Default", is_default: true },
|
|
...devices.filter((d) => d.name !== "Default" && d.name !== "default"),
|
|
];
|
|
|
|
setState((prev) => ({
|
|
...prev,
|
|
audioDevices: devicesWithDefault,
|
|
}));
|
|
} catch (error) {
|
|
console.error("Failed to load audio devices:", error);
|
|
setState((prev) => ({
|
|
...prev,
|
|
audioDevices: [{ index: "default", name: "Default", is_default: true }],
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
// Load output devices
|
|
const loadOutputDevices = useCallback(async () => {
|
|
try {
|
|
const devices: AudioDevice[] = await invoke(
|
|
"get_available_output_devices",
|
|
);
|
|
|
|
// Always ensure "Default" is available as the first option
|
|
const devicesWithDefault = [
|
|
{ index: "default", name: "Default", is_default: true },
|
|
...devices.filter((d) => d.name !== "Default" && d.name !== "default"),
|
|
];
|
|
|
|
setState((prev) => ({
|
|
...prev,
|
|
outputDevices: devicesWithDefault,
|
|
}));
|
|
} catch (error) {
|
|
console.error("Failed to load output devices:", error);
|
|
setState((prev) => ({
|
|
...prev,
|
|
outputDevices: [
|
|
{ index: "default", name: "Default", is_default: true },
|
|
],
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
// Update a specific setting
|
|
const updateSetting = useCallback(
|
|
async <K extends keyof Settings>(key: K, value: Settings[K]) => {
|
|
const updateKey = String(key);
|
|
|
|
// Set updating state
|
|
setState((prev) => ({
|
|
...prev,
|
|
isUpdating: { ...prev.isUpdating, [updateKey]: true },
|
|
}));
|
|
|
|
// Store original value for rollback
|
|
const originalValue = state.settings?.[key];
|
|
|
|
try {
|
|
// Optimistic update
|
|
setState((prev) => ({
|
|
...prev,
|
|
settings: prev.settings ? { ...prev.settings, [key]: value } : null,
|
|
}));
|
|
|
|
// Invoke the appropriate backend method based on the setting
|
|
switch (key) {
|
|
case "always_on_microphone":
|
|
await invoke("update_microphone_mode", { alwaysOn: value });
|
|
break;
|
|
case "audio_feedback":
|
|
await invoke("change_audio_feedback_setting", { enabled: value });
|
|
break;
|
|
case "push_to_talk":
|
|
await invoke("change_ptt_setting", { enabled: value });
|
|
break;
|
|
case "selected_microphone":
|
|
// Map "Default" to "default" for backend compatibility
|
|
const micDeviceName = value === "Default" ? "default" : value;
|
|
await invoke("set_selected_microphone", {
|
|
deviceName: micDeviceName,
|
|
});
|
|
break;
|
|
case "selected_output_device":
|
|
// Map "Default" to "default" for backend compatibility
|
|
const outputDeviceName = value === "Default" ? "default" : value;
|
|
await invoke("set_selected_output_device", {
|
|
deviceName: outputDeviceName,
|
|
});
|
|
break;
|
|
case "translate_to_english":
|
|
await invoke("change_translate_to_english_setting", {
|
|
enabled: value,
|
|
});
|
|
break;
|
|
case "selected_language":
|
|
await invoke("change_selected_language_setting", {
|
|
language: value,
|
|
});
|
|
break;
|
|
case "show_overlay":
|
|
await invoke("change_show_overlay_setting", { enabled: value });
|
|
break;
|
|
case "bindings":
|
|
// Handle bindings separately - they use their own invoke methods
|
|
break;
|
|
case "selected_model":
|
|
// Handle model selection if needed
|
|
break;
|
|
default:
|
|
console.warn(`No handler for setting: ${String(key)}`);
|
|
}
|
|
|
|
console.log(`Setting ${String(key)} updated to:`, value);
|
|
} catch (error) {
|
|
console.error(`Failed to update setting ${String(key)}:`, error);
|
|
|
|
// Rollback on error
|
|
setState((prev) => ({
|
|
...prev,
|
|
settings: prev.settings
|
|
? { ...prev.settings, [key]: originalValue }
|
|
: null,
|
|
}));
|
|
} finally {
|
|
// Clear updating state
|
|
setState((prev) => ({
|
|
...prev,
|
|
isUpdating: { ...prev.isUpdating, [updateKey]: false },
|
|
}));
|
|
}
|
|
},
|
|
[state.settings],
|
|
);
|
|
|
|
// Reset a setting to its default value
|
|
const resetSetting = useCallback(
|
|
async (key: keyof Settings) => {
|
|
// Define default values
|
|
const defaults: Partial<Settings> = {
|
|
always_on_microphone: false,
|
|
audio_feedback: true,
|
|
push_to_talk: false,
|
|
selected_microphone: "Default",
|
|
selected_output_device: "Default",
|
|
translate_to_english: false,
|
|
selected_language: "auto",
|
|
show_overlay: true,
|
|
};
|
|
|
|
const defaultValue = defaults[key];
|
|
if (defaultValue !== undefined) {
|
|
await updateSetting(key, defaultValue as any);
|
|
}
|
|
},
|
|
[updateSetting],
|
|
);
|
|
|
|
// Convenience getter
|
|
const getSetting = useCallback(
|
|
<K extends keyof Settings>(key: K): Settings[K] | undefined => {
|
|
return state.settings?.[key];
|
|
},
|
|
[state.settings],
|
|
);
|
|
|
|
// Update a specific binding
|
|
const updateBinding = useCallback(
|
|
async (id: string, binding: string) => {
|
|
const updateKey = `binding_${id}`;
|
|
|
|
// Set updating state
|
|
setState((prev) => ({
|
|
...prev,
|
|
isUpdating: { ...prev.isUpdating, [updateKey]: true },
|
|
}));
|
|
|
|
// Store original binding for rollback
|
|
const originalBinding = state.settings?.bindings?.[id]?.current_binding;
|
|
|
|
try {
|
|
// Optimistic update
|
|
setState((prev) => ({
|
|
...prev,
|
|
settings: prev.settings
|
|
? {
|
|
...prev.settings,
|
|
bindings: {
|
|
...prev.settings.bindings,
|
|
[id]: {
|
|
...prev.settings.bindings[id],
|
|
current_binding: binding,
|
|
},
|
|
},
|
|
}
|
|
: null,
|
|
}));
|
|
|
|
await invoke("change_binding", { id, binding });
|
|
console.log(`Binding ${id} updated to: ${binding}`);
|
|
} catch (error) {
|
|
console.error(`Failed to update binding ${id}:`, error);
|
|
|
|
// Rollback on error
|
|
if (originalBinding) {
|
|
setState((prev) => ({
|
|
...prev,
|
|
settings: prev.settings
|
|
? {
|
|
...prev.settings,
|
|
bindings: {
|
|
...prev.settings.bindings,
|
|
[id]: {
|
|
...prev.settings.bindings[id],
|
|
current_binding: originalBinding,
|
|
},
|
|
},
|
|
}
|
|
: null,
|
|
}));
|
|
}
|
|
} finally {
|
|
// Clear updating state
|
|
setState((prev) => ({
|
|
...prev,
|
|
isUpdating: { ...prev.isUpdating, [updateKey]: false },
|
|
}));
|
|
}
|
|
},
|
|
[state.settings],
|
|
);
|
|
|
|
// Reset a specific binding
|
|
const resetBinding = useCallback(
|
|
async (id: string) => {
|
|
const updateKey = `binding_${id}`;
|
|
|
|
// Set updating state
|
|
setState((prev) => ({
|
|
...prev,
|
|
isUpdating: { ...prev.isUpdating, [updateKey]: true },
|
|
}));
|
|
|
|
try {
|
|
const result = await invoke("reset_binding", { id });
|
|
|
|
// Refresh settings to get the updated binding
|
|
await loadSettings();
|
|
|
|
console.log(`Binding ${id} reset to default`);
|
|
} catch (error) {
|
|
console.error(`Failed to reset binding ${id}:`, error);
|
|
} finally {
|
|
// Clear updating state
|
|
setState((prev) => ({
|
|
...prev,
|
|
isUpdating: { ...prev.isUpdating, [updateKey]: false },
|
|
}));
|
|
}
|
|
},
|
|
[loadSettings],
|
|
);
|
|
|
|
// Initialize
|
|
useEffect(() => {
|
|
loadSettings();
|
|
loadAudioDevices();
|
|
loadOutputDevices();
|
|
}, [loadSettings, loadAudioDevices, loadOutputDevices]);
|
|
|
|
return {
|
|
settings: state.settings,
|
|
isLoading: state.isLoading,
|
|
isUpdating: (key: string) => state.isUpdating[key] || false,
|
|
audioDevices: state.audioDevices,
|
|
outputDevices: state.outputDevices,
|
|
updateSetting,
|
|
resetSetting,
|
|
refreshSettings: loadSettings,
|
|
refreshAudioDevices: loadAudioDevices,
|
|
refreshOutputDevices: loadOutputDevices,
|
|
updateBinding,
|
|
resetBinding,
|
|
getSetting,
|
|
};
|
|
};
|