Add custom sound and control notif volume (#214)
* Add custom sound and control notif volume * Modularize the picker and slider, consolidate the playback and loading * Move custom sounds to advanced * pairs of audio * custom sound handling --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
d5a970042a
commit
a286fe5143
20 changed files with 394 additions and 149 deletions
BIN
src-tauri/resources/pop_start.wav
Normal file
BIN
src-tauri/resources/pop_start.wav
Normal file
Binary file not shown.
BIN
src-tauri/resources/pop_stop.wav
Normal file
BIN
src-tauri/resources/pop_stop.wav
Normal file
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::audio_feedback::{play_recording_start_sound, play_recording_stop_sound};
|
use crate::audio_feedback::{SoundType, play_feedback_sound};
|
||||||
use crate::managers::audio::AudioRecordingManager;
|
use crate::managers::audio::AudioRecordingManager;
|
||||||
use crate::managers::history::HistoryManager;
|
use crate::managers::history::HistoryManager;
|
||||||
use crate::managers::transcription::TranscriptionManager;
|
use crate::managers::transcription::TranscriptionManager;
|
||||||
|
|
@ -46,7 +46,7 @@ impl ShortcutAction for TranscribeAction {
|
||||||
if is_always_on {
|
if is_always_on {
|
||||||
// Always-on mode: Play audio feedback immediately
|
// Always-on mode: Play audio feedback immediately
|
||||||
debug!("Always-on mode: Playing audio feedback immediately");
|
debug!("Always-on mode: Playing audio feedback immediately");
|
||||||
play_recording_start_sound(app);
|
play_feedback_sound(app, SoundType::Start);
|
||||||
let recording_started = rm.try_start_recording(&binding_id);
|
let recording_started = rm.try_start_recording(&binding_id);
|
||||||
debug!("Recording started: {}", recording_started);
|
debug!("Recording started: {}", recording_started);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -61,7 +61,7 @@ impl ShortcutAction for TranscribeAction {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
debug!("Playing delayed audio feedback");
|
debug!("Playing delayed audio feedback");
|
||||||
play_recording_start_sound(&app_clone);
|
play_feedback_sound(&app_clone, SoundType::Start);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
debug!("Failed to start recording");
|
debug!("Failed to start recording");
|
||||||
|
|
@ -87,7 +87,7 @@ impl ShortcutAction for TranscribeAction {
|
||||||
show_transcribing_overlay(app);
|
show_transcribing_overlay(app);
|
||||||
|
|
||||||
// Play audio feedback for recording stop
|
// Play audio feedback for recording stop
|
||||||
play_recording_stop_sound(app);
|
play_feedback_sound(app, SoundType::Stop);
|
||||||
|
|
||||||
let binding_id = binding_id.to_string(); // Clone binding_id for the async task
|
let binding_id = binding_id.to_string(); // Clone binding_id for the async task
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,19 @@ use std::io::BufReader;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
/// Plays an audio resource from the resources directory.
|
pub enum SoundType {
|
||||||
/// Checks if audio feedback is enabled in settings before playing.
|
Start,
|
||||||
pub fn play_sound(app: &AppHandle, resource_path: &str) {
|
Stop,
|
||||||
// Check if audio feedback is enabled
|
}
|
||||||
let settings = settings::get_settings(app);
|
|
||||||
if !settings.audio_feedback {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Plays an audio resource from the specified directory.
|
||||||
|
fn play_sound(app: &AppHandle, resource_path: &str, base_dir: tauri::path::BaseDirectory) {
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
let resource_path = resource_path.to_string();
|
let resource_path = resource_path.to_string();
|
||||||
|
let volume = settings::get_settings(app).audio_feedback_volume;
|
||||||
|
|
||||||
// Spawn a new thread to play the audio without blocking the main thread
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
// Get the path to the audio file in resources
|
let audio_path = match app_handle.path().resolve(&resource_path, base_dir) {
|
||||||
let audio_path = match app_handle
|
|
||||||
.path()
|
|
||||||
.resolve(&resource_path, tauri::path::BaseDirectory::Resource)
|
|
||||||
{
|
|
||||||
Ok(path) => path.to_path_buf(),
|
Ok(path) => path.to_path_buf(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|
@ -35,38 +29,67 @@ pub fn play_sound(app: &AppHandle, resource_path: &str) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the selected output device from settings
|
|
||||||
let settings = settings::get_settings(&app_handle);
|
let settings = settings::get_settings(&app_handle);
|
||||||
let selected_device = settings.selected_output_device.clone();
|
let selected_device = settings.selected_output_device.clone();
|
||||||
|
|
||||||
// Try to play the audio file
|
if let Err(e) = play_audio_file(&audio_path, selected_device, volume) {
|
||||||
if let Err(e) = play_audio_file(&audio_path, selected_device) {
|
|
||||||
eprintln!("Failed to play sound '{}': {}", resource_path, e);
|
eprintln!("Failed to play sound '{}': {}", resource_path, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to play the recording start sound
|
fn get_sound_path(app: &AppHandle, sound_type: SoundType) -> String {
|
||||||
pub fn play_recording_start_sound(app: &AppHandle) {
|
let settings = settings::get_settings(app);
|
||||||
play_sound(app, "resources/rec_start.wav");
|
match sound_type {
|
||||||
|
SoundType::Start => match settings.sound_theme {
|
||||||
|
crate::settings::SoundTheme::Custom => "custom_start.wav".to_string(),
|
||||||
|
_ => settings.sound_theme.to_start_path(),
|
||||||
|
},
|
||||||
|
SoundType::Stop => match settings.sound_theme {
|
||||||
|
crate::settings::SoundTheme::Custom => "custom_stop.wav".to_string(),
|
||||||
|
_ => settings.sound_theme.to_stop_path(),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to play the recording stop sound
|
pub fn play_feedback_sound(app: &AppHandle, sound_type: SoundType) {
|
||||||
pub fn play_recording_stop_sound(app: &AppHandle) {
|
// Only play if audio feedback is enabled
|
||||||
play_sound(app, "resources/rec_stop.wav");
|
let settings = settings::get_settings(app);
|
||||||
|
if !settings.audio_feedback {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sound_file = get_sound_path(app, sound_type);
|
||||||
|
let base_dir = if settings.sound_theme == crate::settings::SoundTheme::Custom {
|
||||||
|
tauri::path::BaseDirectory::AppData
|
||||||
|
} else {
|
||||||
|
tauri::path::BaseDirectory::Resource
|
||||||
|
};
|
||||||
|
play_sound(app, &sound_file, base_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_test_sound(app: &AppHandle, sound_type: SoundType) {
|
||||||
|
// Always play test sound, regardless of audio_feedback setting
|
||||||
|
let settings = settings::get_settings(app);
|
||||||
|
let sound_file = get_sound_path(app, sound_type);
|
||||||
|
let base_dir = if settings.sound_theme == crate::settings::SoundTheme::Custom {
|
||||||
|
tauri::path::BaseDirectory::AppData
|
||||||
|
} else {
|
||||||
|
tauri::path::BaseDirectory::Resource
|
||||||
|
};
|
||||||
|
play_sound(app, &sound_file, base_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play_audio_file(
|
fn play_audio_file(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
selected_device: Option<String>,
|
selected_device: Option<String>,
|
||||||
|
volume: f32,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let stream_builder = if let Some(device_name) = selected_device {
|
let stream_builder = if let Some(device_name) = selected_device {
|
||||||
if device_name == "Default" {
|
if device_name == "Default" {
|
||||||
println!("Using default device");
|
println!("Using default device");
|
||||||
// Use default device
|
|
||||||
OutputStreamBuilder::from_default_device()?
|
OutputStreamBuilder::from_default_device()?
|
||||||
} else {
|
} else {
|
||||||
// Try to find the device by name
|
|
||||||
let host = crate::audio_toolkit::get_cpal_host();
|
let host = crate::audio_toolkit::get_cpal_host();
|
||||||
let devices = host.output_devices()?;
|
let devices = host.output_devices()?;
|
||||||
|
|
||||||
|
|
@ -88,18 +111,17 @@ fn play_audio_file(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("Using default device");
|
println!("Using default device");
|
||||||
// Use default device
|
|
||||||
OutputStreamBuilder::from_default_device()?
|
OutputStreamBuilder::from_default_device()?
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream_handle = stream_builder.open_stream()?;
|
let stream_handle = stream_builder.open_stream()?;
|
||||||
let mixer = stream_handle.mixer();
|
let mixer = stream_handle.mixer();
|
||||||
|
|
||||||
// Load the audio file
|
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let buf_reader = BufReader::new(file);
|
let buf_reader = BufReader::new(file);
|
||||||
|
|
||||||
let sink = rodio::play(mixer, buf_reader)?;
|
let sink = rodio::play(mixer, buf_reader)?;
|
||||||
|
sink.set_volume(volume);
|
||||||
sink.sleep_until_end();
|
sink.sleep_until_end();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::audio_feedback;
|
||||||
use crate::audio_toolkit::audio::{list_input_devices, list_output_devices};
|
use crate::audio_toolkit::audio::{list_input_devices, list_output_devices};
|
||||||
use crate::managers::audio::{AudioRecordingManager, MicrophoneMode};
|
use crate::managers::audio::{AudioRecordingManager, MicrophoneMode};
|
||||||
use crate::settings::{get_settings, write_settings};
|
use crate::settings::{get_settings, write_settings};
|
||||||
|
|
@ -5,6 +6,29 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct CustomSounds {
|
||||||
|
start: bool,
|
||||||
|
stop: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_sound_exists(app: &AppHandle, sound_type: &str) -> bool {
|
||||||
|
app.path()
|
||||||
|
.resolve(
|
||||||
|
format!("custom_{}.wav", sound_type),
|
||||||
|
tauri::path::BaseDirectory::AppData,
|
||||||
|
)
|
||||||
|
.map_or(false, |path| path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn check_custom_sounds(app: AppHandle) -> CustomSounds {
|
||||||
|
CustomSounds {
|
||||||
|
start: custom_sound_exists(&app, "start"),
|
||||||
|
stop: custom_sound_exists(&app, "stop"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct AudioDevice {
|
pub struct AudioDevice {
|
||||||
pub index: String,
|
pub index: String,
|
||||||
|
|
@ -122,3 +146,16 @@ pub fn get_selected_output_device(app: AppHandle) -> Result<String, String> {
|
||||||
.selected_output_device
|
.selected_output_device
|
||||||
.unwrap_or_else(|| "default".to_string()))
|
.unwrap_or_else(|| "default".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn play_test_sound(app: AppHandle, sound_type: String) {
|
||||||
|
let sound = match sound_type.as_str() {
|
||||||
|
"start" => audio_feedback::SoundType::Start,
|
||||||
|
"stop" => audio_feedback::SoundType::Stop,
|
||||||
|
_ => {
|
||||||
|
eprintln!("Unknown sound type: {}", sound_type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
audio_feedback::play_test_sound(&app, sound);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,8 @@ pub fn run() {
|
||||||
shortcut::reset_binding,
|
shortcut::reset_binding,
|
||||||
shortcut::change_ptt_setting,
|
shortcut::change_ptt_setting,
|
||||||
shortcut::change_audio_feedback_setting,
|
shortcut::change_audio_feedback_setting,
|
||||||
|
shortcut::change_audio_feedback_volume_setting,
|
||||||
|
shortcut::change_sound_theme_setting,
|
||||||
shortcut::change_start_hidden_setting,
|
shortcut::change_start_hidden_setting,
|
||||||
shortcut::change_autostart_setting,
|
shortcut::change_autostart_setting,
|
||||||
shortcut::change_translate_to_english_setting,
|
shortcut::change_translate_to_english_setting,
|
||||||
|
|
@ -245,6 +247,8 @@ pub fn run() {
|
||||||
commands::audio::get_available_output_devices,
|
commands::audio::get_available_output_devices,
|
||||||
commands::audio::set_selected_output_device,
|
commands::audio::set_selected_output_device,
|
||||||
commands::audio::get_selected_output_device,
|
commands::audio::get_selected_output_device,
|
||||||
|
commands::audio::play_test_sound,
|
||||||
|
commands::audio::check_custom_sounds,
|
||||||
commands::transcription::set_model_unload_timeout,
|
commands::transcription::set_model_unload_timeout,
|
||||||
commands::transcription::get_model_load_status,
|
commands::transcription::get_model_load_status,
|
||||||
commands::transcription::unload_model_manually,
|
commands::transcription::unload_model_manually,
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,42 @@ impl ModelUnloadTimeout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SoundTheme {
|
||||||
|
Marimba,
|
||||||
|
Pop,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundTheme {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SoundTheme::Marimba => "marimba",
|
||||||
|
SoundTheme::Pop => "pop",
|
||||||
|
SoundTheme::Custom => "custom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_start_path(&self) -> String {
|
||||||
|
format!("resources/{}_start.wav", self.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_stop_path(&self) -> String {
|
||||||
|
format!("resources/{}_stop.wav", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* still handy for composing the initial JSON in the store ------------- */
|
/* still handy for composing the initial JSON in the store ------------- */
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct AppSettings {
|
pub struct AppSettings {
|
||||||
pub bindings: HashMap<String, ShortcutBinding>,
|
pub bindings: HashMap<String, ShortcutBinding>,
|
||||||
pub push_to_talk: bool,
|
pub push_to_talk: bool,
|
||||||
pub audio_feedback: bool,
|
pub audio_feedback: bool,
|
||||||
|
#[serde(default = "default_audio_feedback_volume")]
|
||||||
|
pub audio_feedback_volume: f32,
|
||||||
|
#[serde(default = "default_sound_theme")]
|
||||||
|
pub sound_theme: SoundTheme,
|
||||||
#[serde(default = "default_start_hidden")]
|
#[serde(default = "default_start_hidden")]
|
||||||
pub start_hidden: bool,
|
pub start_hidden: bool,
|
||||||
#[serde(default = "default_autostart_enabled")]
|
#[serde(default = "default_autostart_enabled")]
|
||||||
|
|
@ -161,6 +191,14 @@ fn default_history_limit() -> usize {
|
||||||
5
|
5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_audio_feedback_volume() -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_sound_theme() -> SoundTheme {
|
||||||
|
SoundTheme::Marimba
|
||||||
|
}
|
||||||
|
|
||||||
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
||||||
|
|
||||||
pub fn get_default_settings() -> AppSettings {
|
pub fn get_default_settings() -> AppSettings {
|
||||||
|
|
@ -189,6 +227,8 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
bindings,
|
bindings,
|
||||||
push_to_talk: true,
|
push_to_talk: true,
|
||||||
audio_feedback: false,
|
audio_feedback: false,
|
||||||
|
audio_feedback_volume: default_audio_feedback_volume(),
|
||||||
|
sound_theme: default_sound_theme(),
|
||||||
start_hidden: default_start_hidden(),
|
start_hidden: default_start_hidden(),
|
||||||
autostart_enabled: default_autostart_enabled(),
|
autostart_enabled: default_autostart_enabled(),
|
||||||
selected_model: "".to_string(),
|
selected_model: "".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
||||||
|
|
||||||
use crate::actions::ACTION_MAP;
|
use crate::actions::ACTION_MAP;
|
||||||
use crate::settings::ShortcutBinding;
|
use crate::settings::ShortcutBinding;
|
||||||
use crate::settings::{self, get_settings, OverlayPosition, PasteMethod};
|
use crate::settings::{self, get_settings, OverlayPosition, PasteMethod, SoundTheme};
|
||||||
use crate::ManagedToggleState;
|
use crate::ManagedToggleState;
|
||||||
|
|
||||||
pub fn init_shortcuts(app: &App) {
|
pub fn init_shortcuts(app: &App) {
|
||||||
|
|
@ -119,6 +119,31 @@ pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<()
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Result<(), String> {
|
||||||
|
let mut settings = settings::get_settings(&app);
|
||||||
|
settings.audio_feedback_volume = volume;
|
||||||
|
settings::write_settings(&app, settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), String> {
|
||||||
|
let mut settings = settings::get_settings(&app);
|
||||||
|
let parsed = match theme.as_str() {
|
||||||
|
"marimba" => SoundTheme::Marimba,
|
||||||
|
"pop" => SoundTheme::Pop,
|
||||||
|
"custom" => SoundTheme::Custom,
|
||||||
|
other => {
|
||||||
|
eprintln!("Invalid sound theme '{}', defaulting to marimba", other);
|
||||||
|
SoundTheme::Marimba
|
||||||
|
}
|
||||||
|
};
|
||||||
|
settings.sound_theme = parsed;
|
||||||
|
settings::write_settings(&app, settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
let mut settings = settings::get_settings(&app);
|
let mut settings = settings::get_settings(&app);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,31 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ToggleSwitch } from "../ui/ToggleSwitch";
|
import { ToggleSwitch } from "../ui/ToggleSwitch";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
import { VolumeSlider } from "./VolumeSlider";
|
||||||
|
import { SoundPicker } from "./SoundPicker";
|
||||||
|
|
||||||
interface AudioFeedbackProps {
|
interface AudioFeedbackProps {
|
||||||
descriptionMode?: "inline" | "tooltip";
|
descriptionMode?: "inline" | "tooltip";
|
||||||
grouped?: boolean;
|
grouped?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AudioFeedback: React.FC<AudioFeedbackProps> = React.memo(({
|
export const AudioFeedback: React.FC<AudioFeedbackProps> = React.memo(
|
||||||
descriptionMode = "tooltip",
|
({ descriptionMode = "tooltip", grouped = false }) => {
|
||||||
grouped = false,
|
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||||
}) => {
|
const audioFeedbackEnabled = getSetting("audio_feedback") || false;
|
||||||
const { getSetting, updateSetting, isUpdating } = useSettings();
|
|
||||||
|
|
||||||
const audioFeedbackEnabled = getSetting("audio_feedback") || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleSwitch
|
<div className="flex flex-col">
|
||||||
checked={audioFeedbackEnabled}
|
<ToggleSwitch
|
||||||
onChange={(enabled) => updateSetting("audio_feedback", enabled)}
|
checked={audioFeedbackEnabled}
|
||||||
isUpdating={isUpdating("audio_feedback")}
|
onChange={(enabled) => updateSetting("audio_feedback", enabled)}
|
||||||
label="Audio Feedback"
|
isUpdating={isUpdating("audio_feedback")}
|
||||||
description="Play sound when recording starts and stops"
|
label="Audio Feedback"
|
||||||
descriptionMode={descriptionMode}
|
description="Play sound when recording starts and stops"
|
||||||
grouped={grouped}
|
descriptionMode={descriptionMode}
|
||||||
/>
|
grouped={grouped}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@ import { SettingsGroup } from "../ui/SettingsGroup";
|
||||||
import { HistoryLimit } from "./HistoryLimit";
|
import { HistoryLimit } from "./HistoryLimit";
|
||||||
import { PasteMethodSetting } from "./PasteMethod";
|
import { PasteMethodSetting } from "./PasteMethod";
|
||||||
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
||||||
|
import { SoundPicker } from "./SoundPicker";
|
||||||
|
|
||||||
export const DebugSettings: React.FC = () => {
|
export const DebugSettings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||||
<SettingsGroup title="Debug">
|
<SettingsGroup title="Debug">
|
||||||
<PasteMethodSetting descriptionMode="tooltip" grouped={true} />
|
<PasteMethodSetting descriptionMode="tooltip" grouped={true} />
|
||||||
|
<SoundPicker
|
||||||
|
label="Sound Theme"
|
||||||
|
description="Choose a sound theme for recording start and stop feedback"
|
||||||
|
/>
|
||||||
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
||||||
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
||||||
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { OutputDeviceSelector } from "./OutputDeviceSelector";
|
||||||
import { PushToTalk } from "./PushToTalk";
|
import { PushToTalk } from "./PushToTalk";
|
||||||
import { AudioFeedback } from "./AudioFeedback";
|
import { AudioFeedback } from "./AudioFeedback";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
import { VolumeSlider } from "./VolumeSlider";
|
||||||
|
|
||||||
export const GeneralSettings: React.FC = () => {
|
export const GeneralSettings: React.FC = () => {
|
||||||
const { audioFeedbackEnabled } = useSettings();
|
const { audioFeedbackEnabled } = useSettings();
|
||||||
|
|
@ -25,6 +26,7 @@ export const GeneralSettings: React.FC = () => {
|
||||||
grouped={true}
|
grouped={true}
|
||||||
disabled={!audioFeedbackEnabled}
|
disabled={!audioFeedbackEnabled}
|
||||||
/>
|
/>
|
||||||
|
<VolumeSlider disabled={!audioFeedbackEnabled} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||||
import { AudioPlayer } from "../ui/AudioPlayer";
|
import { AudioPlayer } from "../ui/AudioPlayer";
|
||||||
import { ClipboardCopy, Star, Check, Trash2, Loader2 } from "lucide-react";
|
import { Copy, Star, Check, Trash2 } from "lucide-react";
|
||||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
|
@ -86,12 +86,12 @@ export const HistorySettings: React.FC = () => {
|
||||||
|
|
||||||
const deleteAudioEntry = async (id: number) => {
|
const deleteAudioEntry = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await invoke("delete_history_entry", {id});
|
await invoke("delete_history_entry", { id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete audio entry:", error);
|
console.error("Failed to delete audio entry:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -189,7 +189,7 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
|
||||||
{showCopied ? (
|
{showCopied ? (
|
||||||
<Check width={16} height={16} />
|
<Check width={16} height={16} />
|
||||||
) : (
|
) : (
|
||||||
<ClipboardCopy width={16} height={16} />
|
<Copy width={16} height={16} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -212,7 +212,7 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
|
||||||
className="text-text/50 hover:text-logo-primary transition-colors cursor-pointer"
|
className="text-text/50 hover:text-logo-primary transition-colors cursor-pointer"
|
||||||
title="Delete entry"
|
title="Delete entry"
|
||||||
>
|
>
|
||||||
<Trash2 width={16} height={16}/>
|
<Trash2 width={16} height={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
67
src/components/settings/SoundPicker.tsx
Normal file
67
src/components/settings/SoundPicker.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "../ui/Button";
|
||||||
|
import { Dropdown, DropdownOption } from "../ui/Dropdown";
|
||||||
|
import { PlayIcon } from "lucide-react";
|
||||||
|
import { SettingContainer } from "../ui/SettingContainer";
|
||||||
|
import { useSettingsStore } from "../../stores/settingsStore";
|
||||||
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
||||||
|
interface SoundPickerProps {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SoundPicker: React.FC<SoundPickerProps> = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
const { getSetting, updateSetting } = useSettings();
|
||||||
|
const playTestSound = useSettingsStore((state) => state.playTestSound);
|
||||||
|
const customSounds = useSettingsStore((state) => state.customSounds);
|
||||||
|
|
||||||
|
const selectedTheme = getSetting("sound_theme") ?? "marimba";
|
||||||
|
|
||||||
|
const options: DropdownOption[] = [
|
||||||
|
{ value: "marimba", label: "Marimba" },
|
||||||
|
{ value: "pop", label: "Pop" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only add Custom option if both custom sound files exist
|
||||||
|
if (customSounds.start && customSounds.stop) {
|
||||||
|
options.push({ value: "custom", label: "Custom" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlayBothSounds = async () => {
|
||||||
|
await playTestSound("start");
|
||||||
|
// Wait before playing stop sound
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
await playTestSound("stop");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContainer
|
||||||
|
title={label}
|
||||||
|
description={description}
|
||||||
|
grouped
|
||||||
|
layout="horizontal"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Dropdown
|
||||||
|
selectedValue={selectedTheme}
|
||||||
|
onSelect={(value) =>
|
||||||
|
updateSetting("sound_theme", value as "marimba" | "pop" | "custom")
|
||||||
|
}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handlePlayBothSounds}
|
||||||
|
title="Preview sound theme (plays start then stop)"
|
||||||
|
>
|
||||||
|
<PlayIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SettingContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
28
src/components/settings/VolumeSlider.tsx
Normal file
28
src/components/settings/VolumeSlider.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Slider } from "../ui/Slider";
|
||||||
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
||||||
|
export const VolumeSlider: React.FC<{ disabled?: boolean }> = ({
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const { getSetting, updateSetting } = useSettings();
|
||||||
|
const audioFeedbackVolume = getSetting("audio_feedback_volume") ?? 0.5;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
value={audioFeedbackVolume}
|
||||||
|
onChange={(value: number) =>
|
||||||
|
updateSetting("audio_feedback_volume", value)
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
label="Volume"
|
||||||
|
description="Adjust the volume of audio feedback sounds"
|
||||||
|
descriptionMode="tooltip"
|
||||||
|
grouped
|
||||||
|
formatValue={(value) => `${Math.round(value * 100)}%`}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||||
export interface DropdownOption {
|
export interface DropdownOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
|
|
@ -97,8 +98,9 @@ export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
selectedValue === option.value
|
selectedValue === option.value
|
||||||
? "bg-logo-primary/20 font-semibold"
|
? "bg-logo-primary/20 font-semibold"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
} ${option.disabled ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||||
onClick={() => handleSelect(option.value)}
|
onClick={() => handleSelect(option.value)}
|
||||||
|
disabled={option.disabled}
|
||||||
>
|
>
|
||||||
<span className="truncate">{option.label}</span>
|
<span className="truncate">{option.label}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export const Slider: React.FC<SliderProps> = ({
|
||||||
descriptionMode={descriptionMode}
|
descriptionMode={descriptionMode}
|
||||||
grouped={grouped}
|
grouped={grouped}
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center space-x-1 h-6">
|
<div className="flex items-center space-x-1 h-6">
|
||||||
|
|
@ -54,11 +55,11 @@ export const Slider: React.FC<SliderProps> = ({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="flex-grow h-2 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-logo-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-grow h-2 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-logo-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to right, #FAA2CA 0%, #FAA2CA ${
|
background: `linear-gradient(to right, var(--color-background-ui) ${
|
||||||
((value - min) / (max - min)) * 100
|
((value - min) / (max - min)) * 100
|
||||||
}%, rgba(128, 128, 128, 0.2) ${
|
}%, rgba(128, 128, 128, 0.2) ${
|
||||||
((value - min) / (max - min)) * 100
|
((value - min) / (max - min)) * 100
|
||||||
}%, rgba(128, 128, 128, 0.2) 100%)`,
|
}%)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{showValue && (
|
{showValue && (
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ export const SettingsSchema = z.object({
|
||||||
bindings: ShortcutBindingsMapSchema,
|
bindings: ShortcutBindingsMapSchema,
|
||||||
push_to_talk: z.boolean(),
|
push_to_talk: z.boolean(),
|
||||||
audio_feedback: z.boolean(),
|
audio_feedback: z.boolean(),
|
||||||
|
audio_feedback_volume: z.number().optional().default(1.0),
|
||||||
|
sound_theme: z
|
||||||
|
.enum(["marimba", "pop", "custom"])
|
||||||
|
.optional()
|
||||||
|
.default("marimba"),
|
||||||
start_hidden: z.boolean().optional().default(false),
|
start_hidden: z.boolean().optional().default(false),
|
||||||
autostart_enabled: z.boolean().optional().default(false),
|
autostart_enabled: z.boolean().optional().default(false),
|
||||||
selected_model: z.string(),
|
selected_model: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface SettingsStore {
|
||||||
isUpdating: Record<string, boolean>;
|
isUpdating: Record<string, boolean>;
|
||||||
audioDevices: AudioDevice[];
|
audioDevices: AudioDevice[];
|
||||||
outputDevices: AudioDevice[];
|
outputDevices: AudioDevice[];
|
||||||
|
customSounds: { start: boolean; stop: boolean };
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
initialize: () => Promise<void>;
|
initialize: () => Promise<void>;
|
||||||
|
|
@ -24,6 +25,8 @@ interface SettingsStore {
|
||||||
resetBinding: (id: string) => Promise<void>;
|
resetBinding: (id: string) => Promise<void>;
|
||||||
getSetting: <K extends keyof Settings>(key: K) => Settings[K] | undefined;
|
getSetting: <K extends keyof Settings>(key: K) => Settings[K] | undefined;
|
||||||
isUpdatingKey: (key: string) => boolean;
|
isUpdatingKey: (key: string) => boolean;
|
||||||
|
playTestSound: (soundType: "start" | "stop") => Promise<void>;
|
||||||
|
checkCustomSounds: () => Promise<void>;
|
||||||
|
|
||||||
// Internal state setters
|
// Internal state setters
|
||||||
setSettings: (settings: Settings | null) => void;
|
setSettings: (settings: Settings | null) => void;
|
||||||
|
|
@ -31,11 +34,14 @@ interface SettingsStore {
|
||||||
setUpdating: (key: string, updating: boolean) => void;
|
setUpdating: (key: string, updating: boolean) => void;
|
||||||
setAudioDevices: (devices: AudioDevice[]) => void;
|
setAudioDevices: (devices: AudioDevice[]) => void;
|
||||||
setOutputDevices: (devices: AudioDevice[]) => void;
|
setOutputDevices: (devices: AudioDevice[]) => void;
|
||||||
|
setCustomSounds: (sounds: { start: boolean; stop: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: Partial<Settings> = {
|
const DEFAULT_SETTINGS: Partial<Settings> = {
|
||||||
always_on_microphone: false,
|
always_on_microphone: false,
|
||||||
audio_feedback: true,
|
audio_feedback: true,
|
||||||
|
audio_feedback_volume: 1.0,
|
||||||
|
sound_theme: "marimba",
|
||||||
start_hidden: false,
|
start_hidden: false,
|
||||||
autostart_enabled: false,
|
autostart_enabled: false,
|
||||||
push_to_talk: false,
|
push_to_talk: false,
|
||||||
|
|
@ -55,6 +61,46 @@ const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||||
is_default: true,
|
is_default: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const settingUpdaters: {
|
||||||
|
[K in keyof Settings]?: (value: Settings[K]) => Promise<unknown>;
|
||||||
|
} = {
|
||||||
|
always_on_microphone: (value) =>
|
||||||
|
invoke("update_microphone_mode", { alwaysOn: value }),
|
||||||
|
audio_feedback: (value) =>
|
||||||
|
invoke("change_audio_feedback_setting", { enabled: value }),
|
||||||
|
audio_feedback_volume: (value) =>
|
||||||
|
invoke("change_audio_feedback_volume_setting", { volume: value }),
|
||||||
|
sound_theme: (value) =>
|
||||||
|
invoke("change_sound_theme_setting", { theme: value }),
|
||||||
|
start_hidden: (value) =>
|
||||||
|
invoke("change_start_hidden_setting", { enabled: value }),
|
||||||
|
autostart_enabled: (value) =>
|
||||||
|
invoke("change_autostart_setting", { enabled: value }),
|
||||||
|
push_to_talk: (value) => invoke("change_ptt_setting", { enabled: value }),
|
||||||
|
selected_microphone: (value) =>
|
||||||
|
invoke("set_selected_microphone", {
|
||||||
|
deviceName: value === "Default" ? "default" : value,
|
||||||
|
}),
|
||||||
|
selected_output_device: (value) =>
|
||||||
|
invoke("set_selected_output_device", {
|
||||||
|
deviceName: value === "Default" ? "default" : value,
|
||||||
|
}),
|
||||||
|
translate_to_english: (value) =>
|
||||||
|
invoke("change_translate_to_english_setting", { enabled: value }),
|
||||||
|
selected_language: (value) =>
|
||||||
|
invoke("change_selected_language_setting", { language: value }),
|
||||||
|
overlay_position: (value) =>
|
||||||
|
invoke("change_overlay_position_setting", { position: value }),
|
||||||
|
debug_mode: (value) =>
|
||||||
|
invoke("change_debug_mode_setting", { enabled: value }),
|
||||||
|
custom_words: (value) => invoke("update_custom_words", { words: value }),
|
||||||
|
word_correction_threshold: (value) =>
|
||||||
|
invoke("change_word_correction_threshold_setting", { threshold: value }),
|
||||||
|
paste_method: (value) =>
|
||||||
|
invoke("change_paste_method_setting", { method: value }),
|
||||||
|
history_limit: (value) => invoke("update_history_limit", { limit: value }),
|
||||||
|
};
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>()(
|
export const useSettingsStore = create<SettingsStore>()(
|
||||||
subscribeWithSelector((set, get) => ({
|
subscribeWithSelector((set, get) => ({
|
||||||
settings: null,
|
settings: null,
|
||||||
|
|
@ -62,6 +108,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
isUpdating: {},
|
isUpdating: {},
|
||||||
audioDevices: [],
|
audioDevices: [],
|
||||||
outputDevices: [],
|
outputDevices: [],
|
||||||
|
customSounds: { start: false, stop: false },
|
||||||
|
|
||||||
// Internal setters
|
// Internal setters
|
||||||
setSettings: (settings) => set({ settings }),
|
setSettings: (settings) => set({ settings }),
|
||||||
|
|
@ -72,6 +119,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
})),
|
})),
|
||||||
setAudioDevices: (audioDevices) => set({ audioDevices }),
|
setAudioDevices: (audioDevices) => set({ audioDevices }),
|
||||||
setOutputDevices: (outputDevices) => set({ outputDevices }),
|
setOutputDevices: (outputDevices) => set({ outputDevices }),
|
||||||
|
setCustomSounds: (customSounds) => set({ customSounds }),
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
getSetting: (key) => get().settings?.[key],
|
getSetting: (key) => get().settings?.[key],
|
||||||
|
|
@ -154,99 +202,52 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Play a test sound
|
||||||
|
playTestSound: async (soundType: "start" | "stop") => {
|
||||||
|
try {
|
||||||
|
await invoke("play_test_sound", { soundType });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to play test sound (${soundType}):`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
checkCustomSounds: async () => {
|
||||||
|
try {
|
||||||
|
const sounds = await invoke("check_custom_sounds");
|
||||||
|
get().setCustomSounds(sounds as { start: boolean; stop: boolean });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check custom sounds:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Update a specific setting
|
// Update a specific setting
|
||||||
updateSetting: async <K extends keyof Settings>(
|
updateSetting: async <K extends keyof Settings>(
|
||||||
key: K,
|
key: K,
|
||||||
value: Settings[K],
|
value: Settings[K],
|
||||||
) => {
|
) => {
|
||||||
const { settings, setUpdating, refreshSettings } = get();
|
const { settings, setUpdating } = get();
|
||||||
const updateKey = String(key);
|
const updateKey = String(key);
|
||||||
const originalValue = settings?.[key];
|
const originalValue = settings?.[key];
|
||||||
|
|
||||||
setUpdating(updateKey, true);
|
setUpdating(updateKey, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistic update
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
settings: state.settings ? { ...state.settings, [key]: value } : null,
|
settings: state.settings ? { ...state.settings, [key]: value } : null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Invoke the appropriate backend method
|
const updater = settingUpdaters[key];
|
||||||
switch (key) {
|
if (updater) {
|
||||||
case "always_on_microphone":
|
await updater(value);
|
||||||
await invoke("update_microphone_mode", { alwaysOn: value });
|
} else if (key !== "bindings" && key !== "selected_model") {
|
||||||
break;
|
console.warn(`No handler for setting: ${String(key)}`);
|
||||||
case "audio_feedback":
|
|
||||||
await invoke("change_audio_feedback_setting", { enabled: value });
|
|
||||||
break;
|
|
||||||
case "start_hidden":
|
|
||||||
await invoke("change_start_hidden_setting", { enabled: value });
|
|
||||||
break;
|
|
||||||
case "autostart_enabled":
|
|
||||||
await invoke("change_autostart_setting", { enabled: value });
|
|
||||||
break;
|
|
||||||
case "push_to_talk":
|
|
||||||
await invoke("change_ptt_setting", { enabled: value });
|
|
||||||
break;
|
|
||||||
case "selected_microphone":
|
|
||||||
const micDeviceName = value === "Default" ? "default" : value;
|
|
||||||
await invoke("set_selected_microphone", {
|
|
||||||
deviceName: micDeviceName,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "selected_output_device":
|
|
||||||
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 "overlay_position":
|
|
||||||
await invoke("change_overlay_position_setting", {
|
|
||||||
position: value,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "debug_mode":
|
|
||||||
await invoke("change_debug_mode_setting", { enabled: value });
|
|
||||||
break;
|
|
||||||
case "custom_words":
|
|
||||||
await invoke("update_custom_words", { words: value });
|
|
||||||
break;
|
|
||||||
case "word_correction_threshold":
|
|
||||||
await invoke("change_word_correction_threshold_setting", {
|
|
||||||
threshold: value,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "paste_method":
|
|
||||||
await invoke("change_paste_method_setting", { method: value });
|
|
||||||
break;
|
|
||||||
case "history_limit":
|
|
||||||
await invoke("update_history_limit", { limit: value });
|
|
||||||
break;
|
|
||||||
case "bindings":
|
|
||||||
case "selected_model":
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(`No handler for setting: ${String(key)}`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to update setting ${String(key)}:`, error);
|
console.error(`Failed to update setting ${String(key)}:`, error);
|
||||||
|
if (settings) {
|
||||||
// Rollback on error
|
set({ settings: { ...settings, [key]: originalValue } });
|
||||||
set((state) => ({
|
}
|
||||||
settings: state.settings
|
|
||||||
? { ...state.settings, [key]: originalValue }
|
|
||||||
: null,
|
|
||||||
}));
|
|
||||||
} finally {
|
} finally {
|
||||||
setUpdating(updateKey, false);
|
setUpdating(updateKey, false);
|
||||||
}
|
}
|
||||||
|
|
@ -273,15 +274,15 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
settings: state.settings
|
settings: state.settings
|
||||||
? {
|
? {
|
||||||
...state.settings,
|
...state.settings,
|
||||||
bindings: {
|
bindings: {
|
||||||
...state.settings.bindings,
|
...state.settings.bindings,
|
||||||
[id]: {
|
[id]: {
|
||||||
...state.settings.bindings[id],
|
...state.settings.bindings[id],
|
||||||
current_binding: binding,
|
current_binding: binding,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -294,15 +295,15 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
settings: state.settings
|
settings: state.settings
|
||||||
? {
|
? {
|
||||||
...state.settings,
|
...state.settings,
|
||||||
bindings: {
|
bindings: {
|
||||||
...state.settings.bindings,
|
...state.settings.bindings,
|
||||||
[id]: {
|
[id]: {
|
||||||
...state.settings.bindings[id],
|
...state.settings.bindings[id],
|
||||||
current_binding: originalBinding,
|
current_binding: originalBinding,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -330,12 +331,17 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
|
|
||||||
// Initialize everything
|
// Initialize everything
|
||||||
initialize: async () => {
|
initialize: async () => {
|
||||||
const { refreshSettings, refreshAudioDevices, refreshOutputDevices } =
|
const {
|
||||||
get();
|
refreshSettings,
|
||||||
|
refreshAudioDevices,
|
||||||
|
refreshOutputDevices,
|
||||||
|
checkCustomSounds,
|
||||||
|
} = get();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
refreshSettings(),
|
refreshSettings(),
|
||||||
refreshAudioDevices(),
|
refreshAudioDevices(),
|
||||||
refreshOutputDevices(),
|
refreshOutputDevices(),
|
||||||
|
checkCustomSounds(),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue