WIP: Add tauri-specta for generating TS bindings (#322)
* initial specta commands * refactor to use the bindings in the ui * merge fixes * remove unused commands * add clamshell to lib * Remove Invoke from everything. * fix settings not being loaded properly * fix settings being overwritten due to deserialization failure + remove provider kind * fix model loading text bug
This commit is contained in:
parent
5085485e67
commit
9521ab9332
42 changed files with 1402 additions and 584 deletions
88
src-tauri/Cargo.lock
generated
88
src-tauri/Cargo.lock
generated
|
|
@ -2,6 +2,12 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
|
|
@ -2524,6 +2530,8 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"signal-hook",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"strsim",
|
||||
"tar",
|
||||
"tauri",
|
||||
|
|
@ -2542,6 +2550,7 @@ dependencies = [
|
|||
"tauri-plugin-sql",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"transcribe-rs",
|
||||
"vad-rs",
|
||||
|
|
@ -4306,6 +4315,12 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
|
|
@ -5907,6 +5922,50 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta"
|
||||
version = "2.0.0-rc.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971"
|
||||
dependencies = [
|
||||
"paste",
|
||||
"specta-macros",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-macros"
|
||||
version = "2.0.0-rc.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-serde"
|
||||
version = "0.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63"
|
||||
dependencies = [
|
||||
"specta",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-typescript"
|
||||
version = "0.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d"
|
||||
dependencies = [
|
||||
"specta",
|
||||
"specta-serde",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
|
|
@ -6557,6 +6616,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_repr",
|
||||
"serialize-to-javascript",
|
||||
"specta",
|
||||
"swift-rs",
|
||||
"tauri-build",
|
||||
"tauri-macros",
|
||||
|
|
@ -6952,6 +7012,34 @@ dependencies = [
|
|||
"wry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-specta"
|
||||
version = "2.0.0-rc.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"tauri",
|
||||
"tauri-specta-macros",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-specta-macros"
|
||||
version = "2.0.0-rc.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.8.0"
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ tar = "0.4.44"
|
|||
flate2 = "1.0"
|
||||
transcribe-rs = "0.1.4"
|
||||
ferrous-opencc = "0.2.3"
|
||||
specta = "=2.0.0-rc.22"
|
||||
specta-typescript = "0.0.9"
|
||||
tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook = "0.3"
|
||||
|
|
|
|||
|
|
@ -40,12 +40,11 @@ fn send_paste_ctrl_v() -> Result<(), String> {
|
|||
|
||||
/// Sends a Shift+Insert paste command (Windows and Linux only).
|
||||
/// This is more universal for terminal applications and legacy software.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn send_paste_shift_insert() -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let insert_key_code = Key::Other(0x2D); // VK_INSERT
|
||||
#[cfg(target_os = "linux")]
|
||||
let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76, also used as fallback)
|
||||
|
||||
let mut enigo = Enigo::new(&Settings::default())
|
||||
.map_err(|e| format!("Failed to initialize Enigo: {}", e))?;
|
||||
|
|
@ -109,7 +108,6 @@ fn paste_via_clipboard_ctrl_v(text: &str, app_handle: &AppHandle) -> Result<(),
|
|||
|
||||
/// Pastes text using the clipboard method with Shift+Insert (Windows/Linux only).
|
||||
/// Saves the current clipboard, writes the text, sends paste command, then restores the clipboard.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn paste_via_clipboard_shift_insert(text: &str, app_handle: &AppHandle) -> Result<(), String> {
|
||||
let clipboard = app_handle.clipboard();
|
||||
|
||||
|
|
@ -145,7 +143,6 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
|||
match paste_method {
|
||||
PasteMethod::CtrlV => paste_via_clipboard_ctrl_v(&text, &app_handle)?,
|
||||
PasteMethod::Direct => paste_via_direct_input(&text)?,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
PasteMethod::ShiftInsert => paste_via_clipboard_shift_insert(&text, &app_handle)?,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ use crate::managers::audio::{AudioRecordingManager, MicrophoneMode};
|
|||
use crate::settings::{get_settings, write_settings};
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct CustomSounds {
|
||||
start: bool,
|
||||
stop: bool,
|
||||
|
|
@ -23,6 +24,7 @@ fn custom_sound_exists(app: &AppHandle, sound_type: &str) -> bool {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn check_custom_sounds(app: AppHandle) -> CustomSounds {
|
||||
CustomSounds {
|
||||
start: custom_sound_exists(&app, "start"),
|
||||
|
|
@ -30,7 +32,7 @@ pub fn check_custom_sounds(app: AppHandle) -> CustomSounds {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
pub struct AudioDevice {
|
||||
pub index: String,
|
||||
pub name: String,
|
||||
|
|
@ -38,6 +40,7 @@ pub struct AudioDevice {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), String> {
|
||||
// Update settings
|
||||
let mut settings = get_settings(&app);
|
||||
|
|
@ -57,12 +60,14 @@ pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), Str
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_microphone_mode(app: AppHandle) -> Result<bool, String> {
|
||||
let settings = get_settings(&app);
|
||||
Ok(settings.always_on_microphone)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_available_microphones() -> Result<Vec<AudioDevice>, String> {
|
||||
let devices =
|
||||
list_input_devices().map_err(|e| format!("Failed to list audio devices: {}", e))?;
|
||||
|
|
@ -83,6 +88,7 @@ pub fn get_available_microphones() -> Result<Vec<AudioDevice>, String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<(), String> {
|
||||
let mut settings = get_settings(&app);
|
||||
settings.selected_microphone = if device_name == "default" {
|
||||
|
|
@ -101,6 +107,7 @@ pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<()
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_selected_microphone(app: AppHandle) -> Result<String, String> {
|
||||
let settings = get_settings(&app);
|
||||
Ok(settings
|
||||
|
|
@ -109,6 +116,7 @@ pub fn get_selected_microphone(app: AppHandle) -> Result<String, String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_available_output_devices() -> Result<Vec<AudioDevice>, String> {
|
||||
let devices =
|
||||
list_output_devices().map_err(|e| format!("Failed to list output devices: {}", e))?;
|
||||
|
|
@ -129,6 +137,7 @@ pub fn get_available_output_devices() -> Result<Vec<AudioDevice>, String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_selected_output_device(app: AppHandle, device_name: String) -> Result<(), String> {
|
||||
let mut settings = get_settings(&app);
|
||||
settings.selected_output_device = if device_name == "default" {
|
||||
|
|
@ -141,6 +150,7 @@ pub fn set_selected_output_device(app: AppHandle, device_name: String) -> Result
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_selected_output_device(app: AppHandle) -> Result<String, String> {
|
||||
let settings = get_settings(&app);
|
||||
Ok(settings
|
||||
|
|
@ -149,6 +159,7 @@ pub fn get_selected_output_device(app: AppHandle) -> Result<String, String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn play_test_sound(app: AppHandle, sound_type: String) {
|
||||
let sound = match sound_type.as_str() {
|
||||
"start" => audio_feedback::SoundType::Start,
|
||||
|
|
@ -162,6 +173,7 @@ pub fn play_test_sound(app: AppHandle, sound_type: String) {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(), String> {
|
||||
let mut settings = get_settings(&app);
|
||||
settings.clamshell_microphone = if device_name == "default" {
|
||||
|
|
@ -174,6 +186,7 @@ pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_clamshell_microphone(app: AppHandle) -> Result<String, String> {
|
||||
let settings = get_settings(&app);
|
||||
Ok(settings
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
|||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_history_entries(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
|
|
@ -14,6 +15,7 @@ pub async fn get_history_entries(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn toggle_history_entry_saved(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
|
|
@ -26,6 +28,7 @@ pub async fn toggle_history_entry_saved(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_audio_file_path(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
|
|
@ -38,6 +41,7 @@ pub async fn get_audio_file_path(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_history_entry(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
|
|
@ -50,6 +54,7 @@ pub async fn delete_history_entry(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn update_history_limit(
|
||||
app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
|
|
@ -67,6 +72,7 @@ pub async fn update_history_limit(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn update_recording_retention_period(
|
||||
app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ pub mod history;
|
|||
pub mod models;
|
||||
pub mod transcription;
|
||||
|
||||
use crate::{settings, utils::cancel_current_operation};
|
||||
use crate::settings::{get_settings, write_settings, AppSettings, LogLevel};
|
||||
use crate::utils::cancel_current_operation;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_log::LogLevel;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn cancel_operation(app: AppHandle) {
|
||||
cancel_current_operation(&app);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
|
|
@ -24,6 +26,19 @@ pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_app_settings(app: AppHandle) -> Result<AppSettings, String> {
|
||||
Ok(get_settings(&app))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_default_settings() -> Result<AppSettings, String> {
|
||||
Ok(crate::settings::get_default_settings())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_log_dir_path(app: AppHandle) -> Result<String, String> {
|
||||
let log_dir = app
|
||||
.path()
|
||||
|
|
@ -33,22 +48,25 @@ pub fn get_log_dir_path(app: AppHandle) -> Result<String, String> {
|
|||
Ok(log_dir.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub fn set_log_level(app: AppHandle, level: LogLevel) -> Result<(), String> {
|
||||
let log_level: log::Level = level.clone().into();
|
||||
let tauri_log_level: tauri_plugin_log::LogLevel = level.into();
|
||||
let log_level: log::Level = tauri_log_level.into();
|
||||
// Update the file log level atomic so the filter picks up the new level
|
||||
crate::FILE_LOG_LEVEL.store(
|
||||
log_level.to_level_filter() as u8,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let mut settings = settings::get_settings(&app);
|
||||
let mut settings = get_settings(&app);
|
||||
settings.log_level = level;
|
||||
settings::write_settings(&app, settings);
|
||||
write_settings(&app, settings);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> {
|
||||
let app_data_dir = app
|
||||
|
|
@ -66,6 +84,7 @@ pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
|
||||
let log_dir = app
|
||||
|
|
@ -81,6 +100,7 @@ pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub fn open_app_data_dir(app: AppHandle) -> Result<(), String> {
|
||||
let app_data_dir = app
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_available_models(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
) -> Result<Vec<ModelInfo>, String> {
|
||||
|
|
@ -12,6 +13,7 @@ pub async fn get_available_models(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_model_info(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
model_id: String,
|
||||
|
|
@ -20,6 +22,7 @@ pub async fn get_model_info(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_model(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
model_id: String,
|
||||
|
|
@ -31,6 +34,7 @@ pub async fn download_model(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_model(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
model_id: String,
|
||||
|
|
@ -41,6 +45,7 @@ pub async fn delete_model(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_active_model(
|
||||
app_handle: AppHandle,
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
|
|
@ -70,12 +75,14 @@ pub async fn set_active_model(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_current_model(app_handle: AppHandle) -> Result<String, String> {
|
||||
let settings = get_settings(&app_handle);
|
||||
Ok(settings.selected_model)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_transcription_model_status(
|
||||
transcription_manager: State<'_, Arc<TranscriptionManager>>,
|
||||
) -> Result<Option<String>, String> {
|
||||
|
|
@ -83,6 +90,7 @@ pub async fn get_transcription_model_status(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_model_loading(
|
||||
transcription_manager: State<'_, Arc<TranscriptionManager>>,
|
||||
) -> Result<bool, String> {
|
||||
|
|
@ -92,6 +100,7 @@ pub async fn is_model_loading(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn has_any_models_available(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
) -> Result<bool, String> {
|
||||
|
|
@ -100,6 +109,7 @@ pub async fn has_any_models_available(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn has_any_models_or_downloads(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
) -> Result<bool, String> {
|
||||
|
|
@ -109,6 +119,7 @@ pub async fn has_any_models_or_downloads(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn cancel_download(
|
||||
model_manager: State<'_, Arc<ModelManager>>,
|
||||
model_id: String,
|
||||
|
|
@ -119,6 +130,7 @@ pub async fn cancel_download(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_recommended_first_model() -> Result<String, String> {
|
||||
// Recommend Parakeet V3 model for first-time users - fastest and most accurate
|
||||
Ok("parakeet-tdt-0.6b-v3".to_string())
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
use crate::managers::transcription::TranscriptionManager;
|
||||
use crate::settings::{get_settings, write_settings, ModelUnloadTimeout};
|
||||
use serde::Serialize;
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct ModelLoadStatus {
|
||||
is_loaded: bool,
|
||||
current_model: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) {
|
||||
let mut settings = get_settings(&app);
|
||||
settings.model_unload_timeout = timeout;
|
||||
|
|
@ -10,19 +19,18 @@ pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_model_load_status(
|
||||
transcription_manager: State<TranscriptionManager>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let is_loaded = transcription_manager.is_model_loaded();
|
||||
let current_model = transcription_manager.get_current_model();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"is_loaded": is_loaded,
|
||||
"current_model": current_model
|
||||
}))
|
||||
) -> Result<ModelLoadStatus, String> {
|
||||
Ok(ModelLoadStatus {
|
||||
is_loaded: transcription_manager.is_model_loaded(),
|
||||
current_model: transcription_manager.get_current_model(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn unload_model_manually(
|
||||
transcription_manager: State<TranscriptionManager>,
|
||||
) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use std::process::Command;
|
|||
/// This queries the macOS IORegistry for the AppleClamshellState key.
|
||||
/// Returns true if the lid is closed, false if open.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tauri::command]
|
||||
pub fn is_clamshell() -> Result<bool, String> {
|
||||
let output = Command::new("ioreg")
|
||||
.args(["-r", "-k", "AppleClamshellState", "-d", "4"])
|
||||
|
|
@ -32,6 +31,7 @@ pub fn is_clamshell() -> Result<bool, String> {
|
|||
/// Returns true if a battery is detected (laptop), false otherwise (desktop)
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn is_laptop() -> Result<bool, String> {
|
||||
let output = Command::new("pmset")
|
||||
.arg("-g")
|
||||
|
|
@ -48,7 +48,6 @@ pub fn is_laptop() -> Result<bool, String> {
|
|||
/// Stub implementation for non-macOS platforms
|
||||
/// Always returns false since clamshell mode is macOS-specific
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[tauri::command]
|
||||
pub fn is_clamshell() -> Result<bool, String> {
|
||||
Ok(false)
|
||||
}
|
||||
|
|
@ -57,6 +56,7 @@ pub fn is_clamshell() -> Result<bool, String> {
|
|||
/// Always returns false since laptop detection is macOS-specific
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn is_laptop() -> Result<bool, String> {
|
||||
Ok(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ mod shortcut;
|
|||
mod signal_handle;
|
||||
mod tray;
|
||||
mod utils;
|
||||
use specta_typescript::{BigIntExportBehavior, Typescript};
|
||||
use tauri_specta::{collect_commands, Builder};
|
||||
|
||||
use env_filter::Builder as EnvFilterBuilder;
|
||||
use managers::audio::AudioRecordingManager;
|
||||
|
|
@ -33,6 +35,8 @@ use tauri::{AppHandle, Manager};
|
|||
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
||||
use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind};
|
||||
|
||||
use crate::settings::get_settings;
|
||||
|
||||
// Global atomic to store the file log level filter
|
||||
// We use u8 to store the log::LevelFilter as a number
|
||||
pub static FILE_LOG_LEVEL: AtomicU8 = AtomicU8::new(log::LevelFilter::Debug as u8);
|
||||
|
|
@ -199,6 +203,7 @@ fn initialize_core_logic(app_handle: &AppHandle) {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
fn trigger_update_check(app: AppHandle) -> Result<(), String> {
|
||||
app.emit("check-for-updates", ())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
|
@ -211,6 +216,90 @@ pub fn run() {
|
|||
// when the variable is unset
|
||||
let console_filter = build_console_filter();
|
||||
|
||||
let specta_builder = Builder::<tauri::Wry>::new().commands(collect_commands![
|
||||
shortcut::change_binding,
|
||||
shortcut::reset_binding,
|
||||
shortcut::change_ptt_setting,
|
||||
shortcut::change_audio_feedback_setting,
|
||||
shortcut::change_audio_feedback_volume_setting,
|
||||
shortcut::change_sound_theme_setting,
|
||||
shortcut::change_start_hidden_setting,
|
||||
shortcut::change_autostart_setting,
|
||||
shortcut::change_translate_to_english_setting,
|
||||
shortcut::change_selected_language_setting,
|
||||
shortcut::change_overlay_position_setting,
|
||||
shortcut::change_debug_mode_setting,
|
||||
shortcut::change_word_correction_threshold_setting,
|
||||
shortcut::change_paste_method_setting,
|
||||
shortcut::change_clipboard_handling_setting,
|
||||
shortcut::change_post_process_enabled_setting,
|
||||
shortcut::change_post_process_base_url_setting,
|
||||
shortcut::change_post_process_api_key_setting,
|
||||
shortcut::change_post_process_model_setting,
|
||||
shortcut::set_post_process_provider,
|
||||
shortcut::fetch_post_process_models,
|
||||
shortcut::add_post_process_prompt,
|
||||
shortcut::update_post_process_prompt,
|
||||
shortcut::delete_post_process_prompt,
|
||||
shortcut::set_post_process_selected_prompt,
|
||||
shortcut::update_custom_words,
|
||||
shortcut::suspend_binding,
|
||||
shortcut::resume_binding,
|
||||
shortcut::change_mute_while_recording_setting,
|
||||
trigger_update_check,
|
||||
commands::cancel_operation,
|
||||
commands::get_app_dir_path,
|
||||
commands::get_app_settings,
|
||||
commands::get_default_settings,
|
||||
commands::get_log_dir_path,
|
||||
commands::set_log_level,
|
||||
commands::open_recordings_folder,
|
||||
commands::open_log_dir,
|
||||
commands::open_app_data_dir,
|
||||
commands::models::get_available_models,
|
||||
commands::models::get_model_info,
|
||||
commands::models::download_model,
|
||||
commands::models::delete_model,
|
||||
commands::models::cancel_download,
|
||||
commands::models::set_active_model,
|
||||
commands::models::get_current_model,
|
||||
commands::models::get_transcription_model_status,
|
||||
commands::models::is_model_loading,
|
||||
commands::models::has_any_models_available,
|
||||
commands::models::has_any_models_or_downloads,
|
||||
commands::models::get_recommended_first_model,
|
||||
commands::audio::update_microphone_mode,
|
||||
commands::audio::get_microphone_mode,
|
||||
commands::audio::get_available_microphones,
|
||||
commands::audio::set_selected_microphone,
|
||||
commands::audio::get_selected_microphone,
|
||||
commands::audio::get_available_output_devices,
|
||||
commands::audio::set_selected_output_device,
|
||||
commands::audio::get_selected_output_device,
|
||||
commands::audio::play_test_sound,
|
||||
commands::audio::check_custom_sounds,
|
||||
commands::audio::set_clamshell_microphone,
|
||||
commands::audio::get_clamshell_microphone,
|
||||
commands::transcription::set_model_unload_timeout,
|
||||
commands::transcription::get_model_load_status,
|
||||
commands::transcription::unload_model_manually,
|
||||
commands::history::get_history_entries,
|
||||
commands::history::toggle_history_entry_saved,
|
||||
commands::history::get_audio_file_path,
|
||||
commands::history::delete_history_entry,
|
||||
commands::history::update_history_limit,
|
||||
commands::history::update_recording_retention_period,
|
||||
helpers::clamshell::is_laptop,
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)] // <- Only export on non-release builds
|
||||
specta_builder
|
||||
.export(
|
||||
Typescript::default().bigint(BigIntExportBehavior::String),
|
||||
"../src/bindings.ts",
|
||||
)
|
||||
.expect("Failed to export typescript bindings");
|
||||
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(
|
||||
LogBuilder::new()
|
||||
|
|
@ -268,8 +357,9 @@ pub fn run() {
|
|||
))
|
||||
.manage(Mutex::new(ShortcutToggleStates::default()))
|
||||
.setup(move |app| {
|
||||
let settings = settings::get_settings(&app.handle());
|
||||
let file_log_level: log::Level = settings.log_level.clone().into();
|
||||
let settings = get_settings(&app.handle());
|
||||
let tauri_log_level: tauri_plugin_log::LogLevel = settings.log_level.into();
|
||||
let file_log_level: log::Level = tauri_log_level.into();
|
||||
// Store the file log level in the atomic for the filter to use
|
||||
FILE_LOG_LEVEL.store(file_log_level.to_level_filter() as u8, Ordering::Relaxed);
|
||||
let app_handle = app.handle().clone();
|
||||
|
|
@ -307,80 +397,7 @@ pub fn run() {
|
|||
}
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
shortcut::change_binding,
|
||||
shortcut::reset_binding,
|
||||
shortcut::change_ptt_setting,
|
||||
shortcut::change_audio_feedback_setting,
|
||||
shortcut::change_audio_feedback_volume_setting,
|
||||
shortcut::change_sound_theme_setting,
|
||||
shortcut::change_start_hidden_setting,
|
||||
shortcut::change_autostart_setting,
|
||||
shortcut::change_translate_to_english_setting,
|
||||
shortcut::change_selected_language_setting,
|
||||
shortcut::change_overlay_position_setting,
|
||||
shortcut::change_debug_mode_setting,
|
||||
shortcut::change_word_correction_threshold_setting,
|
||||
shortcut::change_paste_method_setting,
|
||||
shortcut::change_clipboard_handling_setting,
|
||||
shortcut::change_post_process_enabled_setting,
|
||||
shortcut::change_post_process_base_url_setting,
|
||||
shortcut::change_post_process_api_key_setting,
|
||||
shortcut::change_post_process_model_setting,
|
||||
shortcut::set_post_process_provider,
|
||||
shortcut::fetch_post_process_models,
|
||||
shortcut::add_post_process_prompt,
|
||||
shortcut::update_post_process_prompt,
|
||||
shortcut::delete_post_process_prompt,
|
||||
shortcut::set_post_process_selected_prompt,
|
||||
shortcut::update_custom_words,
|
||||
shortcut::suspend_binding,
|
||||
shortcut::resume_binding,
|
||||
shortcut::change_mute_while_recording_setting,
|
||||
trigger_update_check,
|
||||
commands::cancel_operation,
|
||||
commands::get_app_dir_path,
|
||||
commands::get_log_dir_path,
|
||||
commands::set_log_level,
|
||||
commands::open_recordings_folder,
|
||||
commands::open_log_dir,
|
||||
commands::open_app_data_dir,
|
||||
commands::models::get_available_models,
|
||||
commands::models::get_model_info,
|
||||
commands::models::download_model,
|
||||
commands::models::delete_model,
|
||||
commands::models::cancel_download,
|
||||
commands::models::set_active_model,
|
||||
commands::models::get_current_model,
|
||||
commands::models::get_transcription_model_status,
|
||||
commands::models::is_model_loading,
|
||||
commands::models::has_any_models_available,
|
||||
commands::models::has_any_models_or_downloads,
|
||||
commands::models::get_recommended_first_model,
|
||||
commands::audio::update_microphone_mode,
|
||||
commands::audio::get_microphone_mode,
|
||||
commands::audio::get_available_microphones,
|
||||
commands::audio::set_selected_microphone,
|
||||
commands::audio::get_selected_microphone,
|
||||
commands::audio::get_available_output_devices,
|
||||
commands::audio::set_selected_output_device,
|
||||
commands::audio::get_selected_output_device,
|
||||
commands::audio::play_test_sound,
|
||||
commands::audio::check_custom_sounds,
|
||||
commands::audio::set_clamshell_microphone,
|
||||
commands::audio::get_clamshell_microphone,
|
||||
helpers::clamshell::is_clamshell,
|
||||
helpers::clamshell::is_laptop,
|
||||
commands::transcription::set_model_unload_timeout,
|
||||
commands::transcription::get_model_load_status,
|
||||
commands::transcription::unload_model_manually,
|
||||
commands::history::get_history_entries,
|
||||
commands::history::toggle_history_entry_saved,
|
||||
commands::history::get_audio_file_path,
|
||||
commands::history::delete_history_entry,
|
||||
commands::history::update_history_limit,
|
||||
commands::history::update_recording_retention_period
|
||||
])
|
||||
.invoke_handler(specta_builder.invoke_handler())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use chrono::{DateTime, Local, Utc};
|
|||
use log::{debug, error};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
|
@ -10,7 +11,7 @@ use tauri_plugin_sql::{Migration, MigrationKind};
|
|||
|
||||
use crate::audio_toolkit::save_wav_file;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||||
pub struct HistoryEntry {
|
||||
pub id: i64,
|
||||
pub file_name: String,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use flate2::read::GzDecoder;
|
|||
use futures_util::StreamExt;
|
||||
use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
|
|
@ -13,13 +14,13 @@ use std::sync::Mutex;
|
|||
use tar::Archive;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub enum EngineType {
|
||||
Whisper,
|
||||
Parakeet,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ModelInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
|
|
@ -36,7 +37,7 @@ pub struct ModelInfo {
|
|||
pub speed_score: f32, // 0.0 to 1.0, higher is faster
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DownloadProgress {
|
||||
pub model_id: String,
|
||||
pub downloaded: u64,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,79 @@
|
|||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::de::{self, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use specta::Type;
|
||||
use std::collections::HashMap;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_log::LogLevel;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
// Custom deserializer to handle both old numeric format (1-5) and new string format ("trace", "debug", etc.)
|
||||
impl<'de> Deserialize<'de> for LogLevel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct LogLevelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for LogLevelVisitor {
|
||||
type Value = LogLevel;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or integer representing log level")
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<LogLevel, E> {
|
||||
match value.to_lowercase().as_str() {
|
||||
"trace" => Ok(LogLevel::Trace),
|
||||
"debug" => Ok(LogLevel::Debug),
|
||||
"info" => Ok(LogLevel::Info),
|
||||
"warn" => Ok(LogLevel::Warn),
|
||||
"error" => Ok(LogLevel::Error),
|
||||
_ => Err(E::unknown_variant(
|
||||
value,
|
||||
&["trace", "debug", "info", "warn", "error"],
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, value: u64) -> Result<LogLevel, E> {
|
||||
match value {
|
||||
1 => Ok(LogLevel::Trace),
|
||||
2 => Ok(LogLevel::Debug),
|
||||
3 => Ok(LogLevel::Info),
|
||||
4 => Ok(LogLevel::Warn),
|
||||
5 => Ok(LogLevel::Error),
|
||||
_ => Err(E::invalid_value(de::Unexpected::Unsigned(value), &"1-5")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(LogLevelVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogLevel> for tauri_plugin_log::LogLevel {
|
||||
fn from(level: LogLevel) -> Self {
|
||||
match level {
|
||||
LogLevel::Trace => tauri_plugin_log::LogLevel::Trace,
|
||||
LogLevel::Debug => tauri_plugin_log::LogLevel::Debug,
|
||||
LogLevel::Info => tauri_plugin_log::LogLevel::Info,
|
||||
LogLevel::Warn => tauri_plugin_log::LogLevel::Warn,
|
||||
LogLevel::Error => tauri_plugin_log::LogLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
pub struct ShortcutBinding {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
|
|
@ -14,14 +82,14 @@ pub struct ShortcutBinding {
|
|||
pub current_binding: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
pub struct LLMPrompt {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
pub struct PostProcessProvider {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
|
|
@ -32,7 +100,7 @@ pub struct PostProcessProvider {
|
|||
pub models_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OverlayPosition {
|
||||
None,
|
||||
|
|
@ -40,7 +108,7 @@ pub enum OverlayPosition {
|
|||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModelUnloadTimeout {
|
||||
Never,
|
||||
|
|
@ -53,23 +121,22 @@ pub enum ModelUnloadTimeout {
|
|||
Sec5, // Debug mode only
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PasteMethod {
|
||||
CtrlV,
|
||||
Direct,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
ShiftInsert,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ClipboardHandling {
|
||||
DontModify,
|
||||
CopyToClipboard,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RecordingRetentionPeriod {
|
||||
Never,
|
||||
|
|
@ -125,7 +192,7 @@ impl ModelUnloadTimeout {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoundTheme {
|
||||
Marimba,
|
||||
|
|
@ -152,7 +219,7 @@ impl SoundTheme {
|
|||
}
|
||||
|
||||
/* still handy for composing the initial JSON in the store ------------- */
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
pub struct AppSettings {
|
||||
pub bindings: HashMap<String, ShortcutBinding>,
|
||||
pub push_to_talk: bool,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use log::{error, warn};
|
||||
use serde::Serialize;
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
|
|
@ -22,7 +23,7 @@ pub fn init_shortcuts(app: &AppHandle) {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct BindingResponse {
|
||||
success: bool,
|
||||
binding: Option<ShortcutBinding>,
|
||||
|
|
@ -30,6 +31,7 @@ pub struct BindingResponse {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_binding(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
|
|
@ -93,6 +95,7 @@ pub fn change_binding(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn reset_binding(app: AppHandle, id: String) -> Result<BindingResponse, String> {
|
||||
let binding = settings::get_stored_binding(&app, &id);
|
||||
|
||||
|
|
@ -100,6 +103,7 @@ pub fn reset_binding(app: AppHandle, id: String) -> Result<BindingResponse, Stri
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
|
||||
|
|
@ -113,6 +117,7 @@ pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.audio_feedback = enabled;
|
||||
|
|
@ -121,6 +126,7 @@ pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<()
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
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;
|
||||
|
|
@ -129,6 +135,7 @@ pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Resu
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
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() {
|
||||
|
|
@ -146,6 +153,7 @@ pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), S
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.translate_to_english = enabled;
|
||||
|
|
@ -154,6 +162,7 @@ pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Res
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.selected_language = language;
|
||||
|
|
@ -162,6 +171,7 @@ pub fn change_selected_language_setting(app: AppHandle, language: String) -> Res
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
let parsed = match position.as_str() {
|
||||
|
|
@ -183,6 +193,7 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.debug_mode = enabled;
|
||||
|
|
@ -201,6 +212,7 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.start_hidden = enabled;
|
||||
|
|
@ -219,6 +231,7 @@ pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(),
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.autostart_enabled = enabled;
|
||||
|
|
@ -245,6 +258,7 @@ pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), Str
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.custom_words = words;
|
||||
|
|
@ -253,6 +267,7 @@ pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), Str
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_word_correction_threshold_setting(
|
||||
app: AppHandle,
|
||||
threshold: f64,
|
||||
|
|
@ -264,12 +279,12 @@ pub fn change_word_correction_threshold_setting(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
let parsed = match method.as_str() {
|
||||
"ctrl_v" => PasteMethod::CtrlV,
|
||||
"direct" => PasteMethod::Direct,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"shift_insert" => PasteMethod::ShiftInsert,
|
||||
other => {
|
||||
warn!("Invalid paste method '{}', defaulting to ctrl_v", other);
|
||||
|
|
@ -282,6 +297,7 @@ pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(),
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
let parsed = match handling.as_str() {
|
||||
|
|
@ -301,6 +317,7 @@ pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Re
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.post_process_enabled = enabled;
|
||||
|
|
@ -309,6 +326,7 @@ pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Res
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_post_process_base_url_setting(
|
||||
app: AppHandle,
|
||||
provider_id: String,
|
||||
|
|
@ -352,6 +370,7 @@ fn validate_provider_exists(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_post_process_api_key_setting(
|
||||
app: AppHandle,
|
||||
provider_id: String,
|
||||
|
|
@ -365,6 +384,7 @@ pub fn change_post_process_api_key_setting(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_post_process_model_setting(
|
||||
app: AppHandle,
|
||||
provider_id: String,
|
||||
|
|
@ -378,6 +398,7 @@ pub fn change_post_process_model_setting(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
validate_provider_exists(&settings, &provider_id)?;
|
||||
|
|
@ -387,6 +408,7 @@ pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn add_post_process_prompt(
|
||||
app: AppHandle,
|
||||
name: String,
|
||||
|
|
@ -410,6 +432,7 @@ pub fn add_post_process_prompt(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn update_post_process_prompt(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
|
|
@ -433,6 +456,7 @@ pub fn update_post_process_prompt(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
|
||||
|
|
@ -460,6 +484,7 @@ pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), Stri
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn fetch_post_process_models(
|
||||
app: AppHandle,
|
||||
provider_id: String,
|
||||
|
|
@ -599,6 +624,7 @@ async fn fetch_models_manual(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
|
||||
|
|
@ -613,6 +639,7 @@ pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<()
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.mute_while_recording = enabled;
|
||||
|
|
@ -642,6 +669,7 @@ fn validate_shortcut_string(raw: &str) -> Result<(), String> {
|
|||
/// Temporarily unregister a binding while the user is editing it in the UI.
|
||||
/// This avoids firing the action while keys are being recorded.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
|
||||
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
||||
if let Err(e) = _unregister_shortcut(&app, b) {
|
||||
|
|
@ -654,6 +682,7 @@ pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
|
|||
|
||||
/// Re-register the binding after the user has finished editing.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
|
||||
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
||||
if let Err(e) = _register_shortcut(&app, b) {
|
||||
|
|
|
|||
10
src/App.tsx
10
src/App.tsx
|
|
@ -1,4 +1,3 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import "./App.css";
|
||||
|
|
@ -7,6 +6,7 @@ import Footer from "./components/footer";
|
|||
import Onboarding from "./components/onboarding";
|
||||
import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar";
|
||||
import { useSettings } from "./hooks/useSettings";
|
||||
import { commands } from "@/bindings";
|
||||
|
||||
const renderSettingsContent = (section: SidebarSection) => {
|
||||
const ActiveComponent =
|
||||
|
|
@ -52,8 +52,12 @@ function App() {
|
|||
const checkOnboardingStatus = async () => {
|
||||
try {
|
||||
// Always check if they have any models available
|
||||
const modelsAvailable: boolean = await invoke("has_any_models_available");
|
||||
setShowOnboarding(!modelsAvailable);
|
||||
const result = await commands.hasAnyModelsAvailable();
|
||||
if (result.status === "ok") {
|
||||
setShowOnboarding(!result.data);
|
||||
} else {
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check onboarding status:", error);
|
||||
setShowOnboarding(true);
|
||||
|
|
|
|||
674
src/bindings.ts
Normal file
674
src/bindings.ts
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
/** user-defined commands **/
|
||||
|
||||
|
||||
export const commands = {
|
||||
async changeBinding(id: string, binding: string) : Promise<Result<BindingResponse, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_binding", { id, binding }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async resetBinding(id: string) : Promise<Result<BindingResponse, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("reset_binding", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changePttSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_ptt_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeAudioFeedbackSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_audio_feedback_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeAudioFeedbackVolumeSetting(volume: number) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_audio_feedback_volume_setting", { volume }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeSoundThemeSetting(theme: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_sound_theme_setting", { theme }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeStartHiddenSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_start_hidden_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeAutostartSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_autostart_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeTranslateToEnglishSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_translate_to_english_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeSelectedLanguageSetting(language: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_selected_language_setting", { language }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeOverlayPositionSetting(position: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_overlay_position_setting", { position }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeDebugModeSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_debug_mode_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeWordCorrectionThresholdSetting(threshold: number) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_word_correction_threshold_setting", { threshold }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changePasteMethodSetting(method: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_paste_method_setting", { method }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeClipboardHandlingSetting(handling: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_clipboard_handling_setting", { handling }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changePostProcessEnabledSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_post_process_enabled_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changePostProcessBaseUrlSetting(providerId: string, baseUrl: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_post_process_base_url_setting", { providerId, baseUrl }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changePostProcessApiKeySetting(providerId: string, apiKey: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_post_process_api_key_setting", { providerId, apiKey }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changePostProcessModelSetting(providerId: string, model: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_post_process_model_setting", { providerId, model }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setPostProcessProvider(providerId: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_post_process_provider", { providerId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async fetchPostProcessModels(providerId: string) : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("fetch_post_process_models", { providerId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async addPostProcessPrompt(name: string, prompt: string) : Promise<Result<LLMPrompt, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("add_post_process_prompt", { name, prompt }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async updatePostProcessPrompt(id: string, name: string, prompt: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("update_post_process_prompt", { id, name, prompt }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async deletePostProcessPrompt(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("delete_post_process_prompt", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setPostProcessSelectedPrompt(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_post_process_selected_prompt", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async updateCustomWords(words: string[]) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("update_custom_words", { words }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Temporarily unregister a binding while the user is editing it in the UI.
|
||||
* This avoids firing the action while keys are being recorded.
|
||||
*/
|
||||
async suspendBinding(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("suspend_binding", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Re-register the binding after the user has finished editing.
|
||||
*/
|
||||
async resumeBinding(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("resume_binding", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeMuteWhileRecordingSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_mute_while_recording_setting", { enabled }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async triggerUpdateCheck() : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("trigger_update_check") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async cancelOperation() : Promise<void> {
|
||||
await TAURI_INVOKE("cancel_operation");
|
||||
},
|
||||
async getAppDirPath() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_app_dir_path") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAppSettings() : Promise<Result<AppSettings, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_app_settings") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getDefaultSettings() : Promise<Result<AppSettings, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_default_settings") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getLogDirPath() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_log_dir_path") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setLogLevel(level: LogLevel) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_log_level", { level }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openRecordingsFolder() : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("open_recordings_folder") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openLogDir() : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("open_log_dir") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openAppDataDir() : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("open_app_data_dir") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAvailableModels() : Promise<Result<ModelInfo[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_available_models") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getModelInfo(modelId: string) : Promise<Result<ModelInfo | null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_model_info", { modelId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async downloadModel(modelId: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("download_model", { modelId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async deleteModel(modelId: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("delete_model", { modelId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async cancelDownload(modelId: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("cancel_download", { modelId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setActiveModel(modelId: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_active_model", { modelId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getCurrentModel() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_current_model") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getTranscriptionModelStatus() : Promise<Result<string | null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_transcription_model_status") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async isModelLoading() : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_model_loading") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async hasAnyModelsAvailable() : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("has_any_models_available") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async hasAnyModelsOrDownloads() : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("has_any_models_or_downloads") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getRecommendedFirstModel() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_recommended_first_model") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async updateMicrophoneMode(alwaysOn: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("update_microphone_mode", { alwaysOn }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getMicrophoneMode() : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_microphone_mode") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAvailableMicrophones() : Promise<Result<AudioDevice[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_available_microphones") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setSelectedMicrophone(deviceName: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_selected_microphone", { deviceName }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getSelectedMicrophone() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_selected_microphone") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAvailableOutputDevices() : Promise<Result<AudioDevice[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_available_output_devices") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setSelectedOutputDevice(deviceName: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_selected_output_device", { deviceName }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getSelectedOutputDevice() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_selected_output_device") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async playTestSound(soundType: string) : Promise<void> {
|
||||
await TAURI_INVOKE("play_test_sound", { soundType });
|
||||
},
|
||||
async checkCustomSounds() : Promise<CustomSounds> {
|
||||
return await TAURI_INVOKE("check_custom_sounds");
|
||||
},
|
||||
async setClamshellMicrophone(deviceName: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_clamshell_microphone", { deviceName }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getClamshellMicrophone() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_clamshell_microphone") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setModelUnloadTimeout(timeout: ModelUnloadTimeout) : Promise<void> {
|
||||
await TAURI_INVOKE("set_model_unload_timeout", { timeout });
|
||||
},
|
||||
async getModelLoadStatus() : Promise<Result<ModelLoadStatus, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_model_load_status") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async unloadModelManually() : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("unload_model_manually") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getHistoryEntries() : Promise<Result<HistoryEntry[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_history_entries") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async toggleHistoryEntrySaved(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("toggle_history_entry_saved", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAudioFilePath(fileName: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_audio_file_path", { fileName }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async deleteHistoryEntry(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("delete_history_entry", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async updateHistoryLimit(limit: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("update_history_limit", { limit }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async updateRecordingRetentionPeriod(period: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("update_recording_retention_period", { period }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Checks if the Mac is a laptop by detecting battery presence
|
||||
*
|
||||
* This uses pmset to check for battery information.
|
||||
* Returns true if a battery is detected (laptop), false otherwise (desktop)
|
||||
*/
|
||||
async isLaptop() : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_laptop") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** user-defined events **/
|
||||
|
||||
|
||||
|
||||
/** user-defined constants **/
|
||||
|
||||
|
||||
|
||||
/** user-defined types **/
|
||||
|
||||
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: string; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean }
|
||||
export type AudioDevice = { index: string; name: string; is_default: boolean }
|
||||
export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null }
|
||||
export type ClipboardHandling = "dont_modify" | "copy_to_clipboard"
|
||||
export type CustomSounds = { start: boolean; stop: boolean }
|
||||
export type EngineType = "Whisper" | "Parakeet"
|
||||
export type HistoryEntry = { id: string; file_name: string; timestamp: string; saved: boolean; title: string; transcription_text: string; post_processed_text: string | null; post_process_prompt: string | null }
|
||||
export type LLMPrompt = { id: string; name: string; prompt: string }
|
||||
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error"
|
||||
export type ModelInfo = { id: string; name: string; description: string; filename: string; url: string | null; size_mb: string; is_downloaded: boolean; is_downloading: boolean; partial_size: string; is_directory: boolean; engine_type: EngineType; accuracy_score: number; speed_score: number }
|
||||
export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null }
|
||||
export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5"
|
||||
export type OverlayPosition = "none" | "top" | "bottom"
|
||||
export type PasteMethod = "ctrl_v" | "direct" | "shift_insert"
|
||||
export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null }
|
||||
export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3"
|
||||
export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string }
|
||||
export type SoundTheme = "marimba" | "pop" | "custom"
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
import {
|
||||
invoke as TAURI_INVOKE,
|
||||
Channel as TAURI_CHANNEL,
|
||||
} from "@tauri-apps/api/core";
|
||||
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
|
||||
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
once: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||
emit: null extends T
|
||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
export type Result<T, E> =
|
||||
| { status: "ok"; data: T }
|
||||
| { status: "error"; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(
|
||||
mappings: Record<keyof T, string>,
|
||||
) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
get: (_, event) => {
|
||||
const name = mappings[event as keyof T];
|
||||
|
||||
return new Proxy((() => {}) as any, {
|
||||
apply: (_, __, [window]: [__WebviewWindow__]) => ({
|
||||
listen: (arg: any) => window.listen(name, arg),
|
||||
once: (arg: any) => window.once(name, arg),
|
||||
emit: (arg: any) => window.emit(name, arg),
|
||||
}),
|
||||
get: (_, command: keyof __EventObj__<any>) => {
|
||||
switch (command) {
|
||||
case "listen":
|
||||
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
|
||||
case "once":
|
||||
return (arg: any) => TAURI_API_EVENT.once(name, arg);
|
||||
case "emit":
|
||||
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { ModelInfo } from "../../lib/types";
|
||||
import type { ModelInfo } from "@/bindings";
|
||||
import { formatModelSize } from "../../lib/utils/format";
|
||||
import { ProgressBar } from "../shared";
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
|||
{model.description}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-text/50 tabular-nums">
|
||||
Download size · {formatModelSize(model.size_mb)}
|
||||
Download size · {formatModelSize(Number(model.size_mb))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-logo-primary tabular-nums">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { ModelInfo } from "../../lib/types";
|
||||
import { commands, type ModelInfo } from "@/bindings";
|
||||
import ModelStatusButton from "./ModelStatusButton";
|
||||
import ModelDropdown from "./ModelDropdown";
|
||||
import DownloadProgressDisplay from "./DownloadProgressDisplay";
|
||||
|
|
@ -238,8 +237,10 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const modelList = await invoke<ModelInfo[]>("get_available_models");
|
||||
setModels(modelList);
|
||||
const result = await commands.getAvailableModels();
|
||||
if (result.status === "ok") {
|
||||
setModels(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load models:", err);
|
||||
}
|
||||
|
|
@ -247,21 +248,25 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
|
||||
const loadCurrentModel = async () => {
|
||||
try {
|
||||
const current = await invoke<string>("get_current_model");
|
||||
setCurrentModelId(current);
|
||||
const result = await commands.getCurrentModel();
|
||||
if (result.status === "ok") {
|
||||
const current = result.data;
|
||||
setCurrentModelId(current);
|
||||
|
||||
if (current) {
|
||||
// Check if model is actually loaded
|
||||
const transcriptionStatus = await invoke<string | null>(
|
||||
"get_transcription_model_status",
|
||||
);
|
||||
if (transcriptionStatus === current) {
|
||||
setModelStatus("ready");
|
||||
if (current) {
|
||||
// Check if model is actually loaded
|
||||
const statusResult = await commands.getTranscriptionModelStatus();
|
||||
if (statusResult.status === "ok") {
|
||||
const transcriptionStatus = statusResult.data;
|
||||
if (transcriptionStatus === current) {
|
||||
setModelStatus("ready");
|
||||
} else {
|
||||
setModelStatus("unloaded");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setModelStatus("unloaded");
|
||||
setModelStatus("none");
|
||||
}
|
||||
} else {
|
||||
setModelStatus("none");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load current model:", err);
|
||||
|
|
@ -272,10 +277,16 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
|
||||
const handleModelSelect = async (modelId: string) => {
|
||||
try {
|
||||
setCurrentModelId(modelId); // Set optimistically so loading text shows correct model
|
||||
setModelError(null);
|
||||
setShowModelDropdown(false);
|
||||
await invoke("set_active_model", { modelId });
|
||||
setCurrentModelId(modelId);
|
||||
const result = await commands.setActiveModel(modelId);
|
||||
if (result.status === "error") {
|
||||
const errorMsg = result.error;
|
||||
setModelError(errorMsg);
|
||||
setModelStatus("error");
|
||||
onError?.(errorMsg);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = `${err}`;
|
||||
setModelError(errorMsg);
|
||||
|
|
@ -287,7 +298,13 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
const handleModelDownload = async (modelId: string) => {
|
||||
try {
|
||||
setModelError(null);
|
||||
await invoke("download_model", { modelId });
|
||||
const result = await commands.downloadModel(modelId);
|
||||
if (result.status === "error") {
|
||||
const errorMsg = result.error;
|
||||
setModelError(errorMsg);
|
||||
setModelStatus("error");
|
||||
onError?.(errorMsg);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = `${err}`;
|
||||
setModelError(errorMsg);
|
||||
|
|
@ -347,9 +364,11 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
};
|
||||
|
||||
const handleModelDelete = async (modelId: string) => {
|
||||
await invoke("delete_model", { modelId });
|
||||
await loadModels();
|
||||
setModelError(null);
|
||||
const result = await commands.deleteModel(modelId);
|
||||
if (result.status === "ok") {
|
||||
await loadModels();
|
||||
setModelError(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { Download } from "lucide-react";
|
||||
import { ModelInfo } from "../../lib/types";
|
||||
import type { ModelInfo } from "@/bindings";
|
||||
import { formatModelSize } from "../../lib/utils/format";
|
||||
import Badge from "../ui/Badge";
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
|||
<h3 className="text-lg font-semibold text-text group-hover:text-logo-primary transition-colors">
|
||||
{model.name}
|
||||
</h3>
|
||||
<DownloadSize sizeMb={model.size_mb} />
|
||||
<DownloadSize sizeMb={Number(model.size_mb)} />
|
||||
{isFeatured && <Badge variant="primary">Recommended</Badge>}
|
||||
</div>
|
||||
<p className="text-text/60 text-sm leading-relaxed">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { ModelInfo } from "../../lib/types";
|
||||
import { commands, type ModelInfo } from "@/bindings";
|
||||
import ModelCard from "./ModelCard";
|
||||
import HandyTextLogo from "../icons/HandyTextLogo";
|
||||
|
||||
|
|
@ -19,9 +18,13 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const models: ModelInfo[] = await invoke("get_available_models");
|
||||
// Only show downloadable models for onboarding
|
||||
setAvailableModels(models.filter((m) => !m.is_downloaded));
|
||||
const result = await commands.getAvailableModels();
|
||||
if (result.status === "ok") {
|
||||
// Only show downloadable models for onboarding
|
||||
setAvailableModels(result.data.filter((m) => !m.is_downloaded));
|
||||
} else {
|
||||
setError("Failed to load available models");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load models:", err);
|
||||
setError("Failed to load available models");
|
||||
|
|
@ -36,7 +39,12 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
onModelSelected();
|
||||
|
||||
try {
|
||||
await invoke("download_model", { modelId });
|
||||
const result = await commands.downloadModel(modelId);
|
||||
if (result.status === "error") {
|
||||
console.error("Download failed:", result.error);
|
||||
setError(`Failed to download model: ${result.error}`);
|
||||
setDownloading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Download failed:", err);
|
||||
setError(`Failed to download model: ${err}`);
|
||||
|
|
@ -80,7 +88,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
|
||||
{availableModels
|
||||
.filter((model) => !getRecommendedBadge(model.id))
|
||||
.sort((a, b) => a.size_mb - b.size_mb)
|
||||
.sort((a, b) => Number(a.size_mb) - Number(b.size_mb))
|
||||
.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { commands } from "@/bindings";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { Button } from "../ui/Button";
|
||||
|
||||
|
|
@ -19,8 +19,12 @@ export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
|
|||
useEffect(() => {
|
||||
const loadAppDirectory = async () => {
|
||||
try {
|
||||
const result = await invoke<string>("get_app_dir_path");
|
||||
setAppDirPath(result);
|
||||
const result = await commands.getAppDirPath();
|
||||
if (result.status === "ok") {
|
||||
setAppDirPath(result.data);
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to load app directory",
|
||||
|
|
@ -36,7 +40,7 @@ export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
|
|||
const handleOpen = async () => {
|
||||
if (!appDirPath) return;
|
||||
try {
|
||||
await invoke("open_app_data_dir");
|
||||
await commands.openAppDataDir();
|
||||
} catch (openError) {
|
||||
console.error("Failed to open app data directory:", openError);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { commands } from "@/bindings";
|
||||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { ResetButton } from "../ui/ResetButton";
|
||||
|
|
@ -27,8 +27,12 @@ export const ClamshellMicrophoneSelector: React.FC<ClamshellMicrophoneSelectorPr
|
|||
useEffect(() => {
|
||||
const checkIsLaptop = async () => {
|
||||
try {
|
||||
const result = await invoke<boolean>("is_laptop");
|
||||
setIsLaptop(result);
|
||||
const result = await commands.isLaptop();
|
||||
if (result.status === "ok") {
|
||||
setIsLaptop(result.data);
|
||||
} else {
|
||||
setIsLaptop(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check if device is laptop:", error);
|
||||
setIsLaptop(false);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import type { ClipboardHandling } from "../../lib/types";
|
||||
import type { ClipboardHandling } from "@/bindings";
|
||||
|
||||
interface ClipboardHandlingProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { ResetButton } from "../ui/ResetButton";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { commands } from "@/bindings";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface HandyShortcutProps {
|
||||
|
|
@ -80,7 +80,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
if (editingShortcutId && originalBinding) {
|
||||
try {
|
||||
await updateBinding(editingShortcutId, originalBinding);
|
||||
await invoke("resume_binding", { id: editingShortcutId }).catch(
|
||||
await commands.resumeBinding(editingShortcutId).catch(
|
||||
console.error,
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -88,7 +88,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
toast.error("Failed to restore original shortcut");
|
||||
}
|
||||
} else if (editingShortcutId) {
|
||||
await invoke("resume_binding", { id: editingShortcutId }).catch(
|
||||
await commands.resumeBinding(editingShortcutId).catch(
|
||||
console.error,
|
||||
);
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
try {
|
||||
await updateBinding(editingShortcutId, newShortcut);
|
||||
// Re-register the shortcut now that recording is finished
|
||||
await invoke("resume_binding", { id: editingShortcutId }).catch(
|
||||
await commands.resumeBinding(editingShortcutId).catch(
|
||||
console.error,
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -145,7 +145,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
if (originalBinding) {
|
||||
try {
|
||||
await updateBinding(editingShortcutId, originalBinding);
|
||||
await invoke("resume_binding", { id: editingShortcutId }).catch(
|
||||
await commands.resumeBinding(editingShortcutId).catch(
|
||||
console.error,
|
||||
);
|
||||
} catch (resetError) {
|
||||
|
|
@ -173,7 +173,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
if (editingShortcutId && originalBinding) {
|
||||
try {
|
||||
await updateBinding(editingShortcutId, originalBinding);
|
||||
await invoke("resume_binding", { id: editingShortcutId }).catch(
|
||||
await commands.resumeBinding(editingShortcutId).catch(
|
||||
console.error,
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -181,7 +181,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
toast.error("Failed to restore original shortcut");
|
||||
}
|
||||
} else if (editingShortcutId) {
|
||||
invoke("resume_binding", { id: editingShortcutId }).catch(
|
||||
commands.resumeBinding(editingShortcutId).catch(
|
||||
console.error,
|
||||
);
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
|||
if (editingShortcutId === id) return; // Already editing this shortcut
|
||||
|
||||
// Suspend current binding to avoid firing while recording
|
||||
await invoke("suspend_binding", { id }).catch(console.error);
|
||||
await commands.suspendBinding(id).catch(console.error);
|
||||
|
||||
// Store the original binding to restore if canceled
|
||||
setOriginalBinding(bindings[id]?.current_binding || "");
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ export const HistoryLimit: React.FC<HistoryLimitProps> = ({
|
|||
}) => {
|
||||
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||
|
||||
const historyLimit = getSetting("history_limit") ?? 5;
|
||||
const historyLimit = Number(getSetting("history_limit") ?? "5");
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value, 10);
|
||||
if (!isNaN(value) && value >= 0) {
|
||||
updateSetting("history_limit", value);
|
||||
updateSetting("history_limit", value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { ModelUnloadTimeout } from "../../lib/types";
|
||||
import { commands, type ModelUnloadTimeout } from "@/bindings";
|
||||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
|
||||
|
|
@ -35,7 +34,7 @@ export const ModelUnloadTimeoutSetting: React.FC<ModelUnloadTimeoutProps> = ({
|
|||
const newTimeout = event.target.value as ModelUnloadTimeout;
|
||||
|
||||
try {
|
||||
await invoke("set_model_unload_timeout", { timeout: newTimeout });
|
||||
await commands.setModelUnloadTimeout(newTimeout);
|
||||
updateSetting("model_unload_timeout", newTimeout);
|
||||
} catch (error) {
|
||||
console.error("Failed to update model unload timeout:", error);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Dropdown } from "../ui/Dropdown";
|
|||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { ResetButton } from "../ui/ResetButton";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { AudioDevice } from "../../lib/types";
|
||||
import type { AudioDevice } from "@/bindings";
|
||||
|
||||
interface OutputDeviceSelectorProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { type as getOsType } from "@tauri-apps/plugin-os";
|
|||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import type { PasteMethod } from "../../lib/types";
|
||||
import type { PasteMethod } from "@/bindings";
|
||||
|
||||
interface PasteMethodProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useSettings } from "../../../hooks/useSettings";
|
||||
import { useSettingsStore } from "../../../stores/settingsStore";
|
||||
import type { PostProcessProvider } from "../../../lib/types";
|
||||
import type { PostProcessProvider } from "@/bindings";
|
||||
import type { ModelOption } from "./types";
|
||||
import type { DropdownOption } from "../../ui/Dropdown";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { RecordingRetentionPeriod } from "../../lib/types";
|
||||
import { RecordingRetentionPeriod } from "@/bindings";
|
||||
|
||||
interface RecordingRetentionPeriodProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import type { OverlayPosition } from "../../lib/types";
|
||||
import type { OverlayPosition } from "@/bindings";
|
||||
|
||||
interface ShowOverlayProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { commands } from "@/bindings";
|
||||
import { SettingContainer } from "../../ui/SettingContainer";
|
||||
import { Button } from "../../ui/Button";
|
||||
|
||||
|
|
@ -19,8 +19,12 @@ export const LogDirectory: React.FC<LogDirectoryProps> = ({
|
|||
useEffect(() => {
|
||||
const loadLogDirectory = async () => {
|
||||
try {
|
||||
const result = await invoke<string>("get_log_dir_path");
|
||||
setLogDir(result);
|
||||
const result = await commands.getLogDirPath();
|
||||
if (result.status === "ok") {
|
||||
setLogDir(result.data);
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err && typeof err === "object" && "message" in err
|
||||
|
|
@ -38,7 +42,7 @@ export const LogDirectory: React.FC<LogDirectoryProps> = ({
|
|||
const handleOpen = async () => {
|
||||
if (!logDir) return;
|
||||
try {
|
||||
await invoke("open_log_dir");
|
||||
await commands.openLogDir();
|
||||
} catch (openError) {
|
||||
console.error("Failed to open log directory:", openError);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import React from "react";
|
|||
import { SettingContainer } from "../../ui/SettingContainer";
|
||||
import { Dropdown, type DropdownOption } from "../../ui/Dropdown";
|
||||
import { useSettings } from "../../../hooks/useSettings";
|
||||
import type { LogLevel } from "../../../bindings";
|
||||
|
||||
const LOG_LEVEL_OPTIONS: DropdownOption[] = [
|
||||
{ value: "5", label: "Error" },
|
||||
{ value: "4", label: "Warn" },
|
||||
{ value: "3", label: "Info" },
|
||||
{ value: "2", label: "Debug" },
|
||||
{ value: "1", label: "Trace" },
|
||||
{ value: "error", label: "Error" },
|
||||
{ value: "warn", label: "Warn" },
|
||||
{ value: "info", label: "Info" },
|
||||
{ value: "debug", label: "Debug" },
|
||||
{ value: "trace", label: "Trace" },
|
||||
];
|
||||
|
||||
interface LogLevelSelectorProps {
|
||||
|
|
@ -21,19 +22,13 @@ export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
|
|||
grouped = false,
|
||||
}) => {
|
||||
const { settings, updateSetting, isUpdating } = useSettings();
|
||||
const currentLevel = settings?.log_level ?? 2;
|
||||
const isLevelUpdating = isUpdating("log_level");
|
||||
|
||||
const selectedValue = currentLevel.toString();
|
||||
const currentLevel = settings?.log_level ?? "debug";
|
||||
|
||||
const handleSelect = async (value: string) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed === currentLevel) {
|
||||
return;
|
||||
}
|
||||
if (value === currentLevel) return;
|
||||
|
||||
try {
|
||||
await updateSetting("log_level", parsed as typeof currentLevel);
|
||||
await updateSetting("log_level", value as LogLevel);
|
||||
} catch (error) {
|
||||
console.error("Failed to update log level:", error);
|
||||
}
|
||||
|
|
@ -47,14 +42,12 @@ export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
|
|||
grouped={grouped}
|
||||
layout="horizontal"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Dropdown
|
||||
options={LOG_LEVEL_OPTIONS}
|
||||
selectedValue={selectedValue}
|
||||
onSelect={handleSelect}
|
||||
disabled={!settings || isLevelUpdating}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
options={LOG_LEVEL_OPTIONS}
|
||||
selectedValue={currentLevel}
|
||||
onSelect={handleSelect}
|
||||
disabled={!settings || isUpdating("log_level")}
|
||||
/>
|
||||
</SettingContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,17 +2,9 @@ import React, { useState, useEffect, useCallback } from "react";
|
|||
import { AudioPlayer } from "../../ui/AudioPlayer";
|
||||
import { Button } from "../../ui/Button";
|
||||
import { Copy, Star, Check, Trash2, FolderOpen } from "lucide-react";
|
||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
interface HistoryEntry {
|
||||
id: number;
|
||||
file_name: string;
|
||||
timestamp: number;
|
||||
saved: boolean;
|
||||
title: string;
|
||||
transcription_text: string;
|
||||
}
|
||||
import { commands, type HistoryEntry } from "@/bindings";
|
||||
|
||||
interface OpenRecordingsButtonProps {
|
||||
onClick: () => void;
|
||||
|
|
@ -39,8 +31,10 @@ export const HistorySettings: React.FC = () => {
|
|||
|
||||
const loadHistoryEntries = useCallback(async () => {
|
||||
try {
|
||||
const entries = await invoke<HistoryEntry[]>("get_history_entries");
|
||||
setHistoryEntries(entries);
|
||||
const result = await commands.getHistoryEntries();
|
||||
if (result.status === "ok") {
|
||||
setHistoryEntries(result.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load history entries:", error);
|
||||
} finally {
|
||||
|
|
@ -73,9 +67,9 @@ export const HistorySettings: React.FC = () => {
|
|||
};
|
||||
}, [loadHistoryEntries]);
|
||||
|
||||
const toggleSaved = async (id: number) => {
|
||||
const toggleSaved = async (id: string) => {
|
||||
try {
|
||||
await invoke("toggle_history_entry_saved", { id });
|
||||
await commands.toggleHistoryEntrySaved(id);
|
||||
// No need to reload here - the event listener will handle it
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle saved status:", error);
|
||||
|
|
@ -92,20 +86,20 @@ export const HistorySettings: React.FC = () => {
|
|||
|
||||
const getAudioUrl = async (fileName: string) => {
|
||||
try {
|
||||
const filePath = await invoke<string>("get_audio_file_path", {
|
||||
fileName,
|
||||
});
|
||||
|
||||
return convertFileSrc(`${filePath}`, "asset");
|
||||
const result = await commands.getAudioFilePath(fileName);
|
||||
if (result.status === "ok") {
|
||||
return convertFileSrc(`${result.data}`, "asset");
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Failed to get audio file path:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAudioEntry = async (id: number) => {
|
||||
const deleteAudioEntry = async (id: string) => {
|
||||
try {
|
||||
await invoke("delete_history_entry", { id });
|
||||
await commands.deleteHistoryEntry(id);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete audio entry:", error);
|
||||
throw error;
|
||||
|
|
@ -114,7 +108,7 @@ export const HistorySettings: React.FC = () => {
|
|||
|
||||
const openRecordingsFolder = async () => {
|
||||
try {
|
||||
await invoke("open_recordings_folder");
|
||||
await commands.openRecordingsFolder();
|
||||
} catch (error) {
|
||||
console.error("Failed to open recordings folder:", error);
|
||||
}
|
||||
|
|
@ -199,7 +193,7 @@ interface HistoryEntryProps {
|
|||
onToggleSaved: () => void;
|
||||
onCopyText: () => void;
|
||||
getAudioUrl: (fileName: string) => Promise<string | null>;
|
||||
deleteAudio: (id: number) => Promise<void>;
|
||||
deleteAudio: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { commands } from "@/bindings";
|
||||
|
||||
import { SettingsGroup } from "../../ui/SettingsGroup";
|
||||
import { SettingContainer } from "../../ui/SettingContainer";
|
||||
|
|
@ -16,7 +16,7 @@ import { ApiKeyField } from "../PostProcessingSettingsApi/ApiKeyField";
|
|||
import { ModelSelect } from "../PostProcessingSettingsApi/ModelSelect";
|
||||
import { usePostProcessProviderState } from "../PostProcessingSettingsApi/usePostProcessProviderState";
|
||||
import { useSettings } from "../../../hooks/useSettings";
|
||||
import type { LLMPrompt } from "../../../lib/types";
|
||||
import type { LLMPrompt } from "@/bindings";
|
||||
|
||||
const DisabledNotice: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
|
|
@ -178,13 +178,15 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
|
|||
if (!draftName.trim() || !draftText.trim()) return;
|
||||
|
||||
try {
|
||||
const newPrompt = await invoke<LLMPrompt>("add_post_process_prompt", {
|
||||
name: draftName.trim(),
|
||||
prompt: draftText.trim(),
|
||||
});
|
||||
await refreshSettings();
|
||||
updateSetting("post_process_selected_prompt_id", newPrompt.id);
|
||||
setIsCreating(false);
|
||||
const result = await commands.addPostProcessPrompt(
|
||||
draftName.trim(),
|
||||
draftText.trim()
|
||||
);
|
||||
if (result.status === "ok") {
|
||||
await refreshSettings();
|
||||
updateSetting("post_process_selected_prompt_id", result.data.id);
|
||||
setIsCreating(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create prompt:", error);
|
||||
}
|
||||
|
|
@ -194,11 +196,11 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
|
|||
if (!selectedPromptId || !draftName.trim() || !draftText.trim()) return;
|
||||
|
||||
try {
|
||||
await invoke("update_post_process_prompt", {
|
||||
id: selectedPromptId,
|
||||
name: draftName.trim(),
|
||||
prompt: draftText.trim(),
|
||||
});
|
||||
await commands.updatePostProcessPrompt(
|
||||
selectedPromptId,
|
||||
draftName.trim(),
|
||||
draftText.trim()
|
||||
);
|
||||
await refreshSettings();
|
||||
} catch (error) {
|
||||
console.error("Failed to update prompt:", error);
|
||||
|
|
@ -209,7 +211,7 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
|
|||
if (!promptId) return;
|
||||
|
||||
try {
|
||||
await invoke("delete_post_process_prompt", { id: promptId });
|
||||
await commands.deletePostProcessPrompt(promptId);
|
||||
await refreshSettings();
|
||||
setIsCreating(false);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
filename: string;
|
||||
url?: string;
|
||||
size_mb: number;
|
||||
is_downloaded: boolean;
|
||||
is_downloading: boolean;
|
||||
partial_size: number;
|
||||
is_directory: boolean;
|
||||
accuracy_score: number;
|
||||
speed_score: number;
|
||||
}
|
||||
import { commands, type ModelInfo } from "@/bindings";
|
||||
|
||||
interface DownloadProgress {
|
||||
model_id: string;
|
||||
|
|
@ -43,9 +28,13 @@ export const useModels = () => {
|
|||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const modelList = await invoke<ModelInfo[]>("get_available_models");
|
||||
setModels(modelList);
|
||||
setError(null);
|
||||
const result = await commands.getAvailableModels();
|
||||
if (result.status === "ok") {
|
||||
setModels(result.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(`Failed to load models: ${result.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Failed to load models: ${err}`);
|
||||
} finally {
|
||||
|
|
@ -55,8 +44,10 @@ export const useModels = () => {
|
|||
|
||||
const loadCurrentModel = async () => {
|
||||
try {
|
||||
const current = await invoke<string>("get_current_model");
|
||||
setCurrentModel(current);
|
||||
const result = await commands.getCurrentModel();
|
||||
if (result.status === "ok") {
|
||||
setCurrentModel(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load current model:", err);
|
||||
}
|
||||
|
|
@ -64,10 +55,14 @@ export const useModels = () => {
|
|||
|
||||
const checkFirstRun = async () => {
|
||||
try {
|
||||
const hasModels = await invoke<boolean>("has_any_models_available");
|
||||
setHasAnyModels(hasModels);
|
||||
setIsFirstRun(!hasModels);
|
||||
return !hasModels;
|
||||
const result = await commands.hasAnyModelsAvailable();
|
||||
if (result.status === "ok") {
|
||||
const hasModels = result.data;
|
||||
setHasAnyModels(hasModels);
|
||||
setIsFirstRun(!hasModels);
|
||||
return !hasModels;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error("Failed to check model availability:", err);
|
||||
return false;
|
||||
|
|
@ -77,11 +72,16 @@ export const useModels = () => {
|
|||
const selectModel = async (modelId: string) => {
|
||||
try {
|
||||
setError(null);
|
||||
await invoke("set_active_model", { modelId });
|
||||
setCurrentModel(modelId);
|
||||
setIsFirstRun(false);
|
||||
setHasAnyModels(true);
|
||||
return true;
|
||||
const result = await commands.setActiveModel(modelId);
|
||||
if (result.status === "ok") {
|
||||
setCurrentModel(modelId);
|
||||
setIsFirstRun(false);
|
||||
setHasAnyModels(true);
|
||||
return true;
|
||||
} else {
|
||||
setError(`Failed to switch to model: ${result.error}`);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Failed to switch to model: ${err}`);
|
||||
return false;
|
||||
|
|
@ -92,8 +92,18 @@ export const useModels = () => {
|
|||
try {
|
||||
setError(null);
|
||||
setDownloadingModels((prev) => new Set(prev.add(modelId)));
|
||||
await invoke("download_model", { modelId });
|
||||
return true;
|
||||
const result = await commands.downloadModel(modelId);
|
||||
if (result.status === "ok") {
|
||||
return true;
|
||||
} else {
|
||||
setError(`Failed to download model: ${result.error}`);
|
||||
setDownloadingModels((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(modelId);
|
||||
return next;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Failed to download model: ${err}`);
|
||||
setDownloadingModels((prev) => {
|
||||
|
|
@ -108,9 +118,14 @@ export const useModels = () => {
|
|||
const deleteModel = async (modelId: string) => {
|
||||
try {
|
||||
setError(null);
|
||||
await invoke("delete_model", { modelId });
|
||||
await loadModels(); // Refresh the list
|
||||
return true;
|
||||
const result = await commands.deleteModel(modelId);
|
||||
if (result.status === "ok") {
|
||||
await loadModels(); // Refresh the list
|
||||
return true;
|
||||
} else {
|
||||
setError(`Failed to delete model: ${result.error}`);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Failed to delete model: ${err}`);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
import { useSettingsStore } from "../stores/settingsStore";
|
||||
import { Settings, AudioDevice } from "../lib/types";
|
||||
import type { AppSettings as Settings, AudioDevice } from "@/bindings";
|
||||
|
||||
interface UseSettingsReturn {
|
||||
// State
|
||||
|
|
|
|||
151
src/lib/types.ts
151
src/lib/types.ts
|
|
@ -1,151 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ShortcutBindingSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
default_binding: z.string(),
|
||||
current_binding: z.string(),
|
||||
});
|
||||
|
||||
export const ShortcutBindingsMapSchema = z.record(
|
||||
z.string(),
|
||||
ShortcutBindingSchema,
|
||||
);
|
||||
|
||||
export const AudioDeviceSchema = z.object({
|
||||
index: z.string(),
|
||||
name: z.string(),
|
||||
is_default: z.boolean(),
|
||||
});
|
||||
|
||||
export const OverlayPositionSchema = z.enum(["none", "top", "bottom"]);
|
||||
export type OverlayPosition = z.infer<typeof OverlayPositionSchema>;
|
||||
|
||||
export const ModelUnloadTimeoutSchema = z.enum([
|
||||
"never",
|
||||
"immediately",
|
||||
"min2",
|
||||
"min5",
|
||||
"min10",
|
||||
"min15",
|
||||
"hour1",
|
||||
"sec5",
|
||||
]);
|
||||
export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
|
||||
|
||||
export const PasteMethodSchema = z.enum(["ctrl_v", "direct", "shift_insert"]);
|
||||
export type PasteMethod = z.infer<typeof PasteMethodSchema>;
|
||||
|
||||
export const ClipboardHandlingSchema = z.enum([
|
||||
"dont_modify",
|
||||
"copy_to_clipboard",
|
||||
]);
|
||||
export type ClipboardHandling = z.infer<typeof ClipboardHandlingSchema>;
|
||||
|
||||
export const LogLevelSchema = z.number().int().min(1).max(5).default(2);
|
||||
export type LogLevelValue = z.infer<typeof LogLevelSchema>;
|
||||
|
||||
export const RecordingRetentionPeriodSchema = z.enum([
|
||||
"never",
|
||||
"preserve_limit",
|
||||
"days3",
|
||||
"weeks2",
|
||||
"months3",
|
||||
]);
|
||||
export type RecordingRetentionPeriod = z.infer<
|
||||
typeof RecordingRetentionPeriodSchema
|
||||
>;
|
||||
|
||||
export const LLMPromptSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
prompt: z.string(),
|
||||
});
|
||||
|
||||
export type LLMPrompt = z.infer<typeof LLMPromptSchema>;
|
||||
|
||||
export const PostProcessProviderSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
base_url: z.string(),
|
||||
allow_base_url_edit: z.boolean().optional().default(false),
|
||||
models_endpoint: z.string().nullable().optional(),
|
||||
kind: z
|
||||
.enum(["openai_compatible", "anthropic"])
|
||||
.optional()
|
||||
.default("openai_compatible"),
|
||||
});
|
||||
|
||||
export type PostProcessProvider = z.infer<typeof PostProcessProviderSchema>;
|
||||
|
||||
export const SettingsSchema = z.object({
|
||||
bindings: ShortcutBindingsMapSchema,
|
||||
push_to_talk: 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),
|
||||
autostart_enabled: z.boolean().optional().default(false),
|
||||
selected_model: z.string(),
|
||||
always_on_microphone: z.boolean(),
|
||||
selected_microphone: z.string().nullable().optional(),
|
||||
clamshell_microphone: z.string().nullable().optional(),
|
||||
selected_output_device: z.string().nullable().optional(),
|
||||
translate_to_english: z.boolean(),
|
||||
selected_language: z.string(),
|
||||
overlay_position: OverlayPositionSchema,
|
||||
debug_mode: z.boolean(),
|
||||
log_level: LogLevelSchema.optional().default(2),
|
||||
custom_words: z.array(z.string()).optional().default([]),
|
||||
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
|
||||
word_correction_threshold: z.number().optional().default(0.18),
|
||||
history_limit: z.number().optional().default(5),
|
||||
recording_retention_period:
|
||||
RecordingRetentionPeriodSchema.optional().default("preserve_limit"),
|
||||
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
||||
clipboard_handling: ClipboardHandlingSchema.optional().default("dont_modify"),
|
||||
post_process_enabled: z.boolean().optional().default(false),
|
||||
post_process_provider_id: z.string().optional().default("openai"),
|
||||
post_process_providers: z
|
||||
.array(PostProcessProviderSchema)
|
||||
.optional()
|
||||
.default([]),
|
||||
post_process_api_keys: z.record(z.string()).optional().default({}),
|
||||
post_process_models: z.record(z.string()).optional().default({}),
|
||||
post_process_prompts: z.array(LLMPromptSchema).optional().default([]),
|
||||
post_process_selected_prompt_id: z.string().nullable().optional(),
|
||||
mute_while_recording: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const BindingResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
binding: ShortcutBindingSchema.nullable(),
|
||||
error: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type AudioDevice = z.infer<typeof AudioDeviceSchema>;
|
||||
export type BindingResponse = z.infer<typeof BindingResponseSchema>;
|
||||
export type ShortcutBinding = z.infer<typeof ShortcutBindingSchema>;
|
||||
export type ShortcutBindingsMap = z.infer<typeof ShortcutBindingsMapSchema>;
|
||||
export type Settings = z.infer<typeof SettingsSchema>;
|
||||
|
||||
export const ModelInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
filename: z.string(),
|
||||
url: z.string().optional(),
|
||||
size_mb: z.number(),
|
||||
is_downloaded: z.boolean(),
|
||||
is_downloading: z.boolean(),
|
||||
partial_size: z.number(),
|
||||
is_directory: z.boolean(),
|
||||
accuracy_score: z.number(),
|
||||
speed_score: z.number(),
|
||||
});
|
||||
|
||||
export type ModelInfo = z.infer<typeof ModelInfoSchema>;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
|
|
@ -7,6 +6,7 @@ import {
|
|||
CancelIcon,
|
||||
} from "../components/icons";
|
||||
import "./RecordingOverlay.css";
|
||||
import { commands } from "@/bindings";
|
||||
|
||||
type OverlayState = "recording" | "transcribing";
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ const RecordingOverlay: React.FC = () => {
|
|||
<div
|
||||
className="cancel-button"
|
||||
onClick={() => {
|
||||
invoke("cancel_operation");
|
||||
commands.cancelOperation();
|
||||
}}
|
||||
>
|
||||
<CancelIcon />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { create } from "zustand";
|
||||
import { subscribeWithSelector } from "zustand/middleware";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Settings, AudioDevice } from "../lib/types";
|
||||
import type { AppSettings as Settings, AudioDevice } from "@/bindings";
|
||||
import { commands } from "@/bindings";
|
||||
|
||||
interface SettingsStore {
|
||||
settings: Settings | null;
|
||||
defaultSettings: Settings | null;
|
||||
isLoading: boolean;
|
||||
isUpdating: Record<string, boolean>;
|
||||
audioDevices: AudioDevice[];
|
||||
|
|
@ -14,6 +15,7 @@ interface SettingsStore {
|
|||
|
||||
// Actions
|
||||
initialize: () => Promise<void>;
|
||||
loadDefaultSettings: () => Promise<void>;
|
||||
updateSetting: <K extends keyof Settings>(
|
||||
key: K,
|
||||
value: Settings[K],
|
||||
|
|
@ -48,6 +50,7 @@ interface SettingsStore {
|
|||
|
||||
// Internal state setters
|
||||
setSettings: (settings: Settings | null) => void;
|
||||
setDefaultSettings: (defaultSettings: Settings | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setUpdating: (key: string, updating: boolean) => void;
|
||||
setAudioDevices: (devices: AudioDevice[]) => void;
|
||||
|
|
@ -55,29 +58,8 @@ interface SettingsStore {
|
|||
setCustomSounds: (sounds: { start: boolean; stop: boolean }) => void;
|
||||
}
|
||||
|
||||
// Note: Default post-processing settings are now managed in Rust
|
||||
// Settings will always have providers after migration
|
||||
const DEFAULT_SETTINGS: Partial<Settings> = {
|
||||
always_on_microphone: false,
|
||||
audio_feedback: true,
|
||||
audio_feedback_volume: 1.0,
|
||||
sound_theme: "marimba",
|
||||
start_hidden: false,
|
||||
autostart_enabled: false,
|
||||
push_to_talk: false,
|
||||
selected_microphone: "Default",
|
||||
clamshell_microphone: "Default",
|
||||
selected_output_device: "Default",
|
||||
translate_to_english: false,
|
||||
selected_language: "auto",
|
||||
overlay_position: "bottom",
|
||||
debug_mode: false,
|
||||
log_level: 2,
|
||||
custom_words: [],
|
||||
history_limit: 5,
|
||||
recording_retention_period: "preserve_limit",
|
||||
mute_while_recording: false,
|
||||
};
|
||||
// Note: Default settings are now fetched from Rust via commands.getDefaultSettings()
|
||||
// This ensures platform-specific defaults (like overlay_position, shortcuts, paste_method) work correctly
|
||||
|
||||
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||
index: "default",
|
||||
|
|
@ -89,60 +71,61 @@ const settingUpdaters: {
|
|||
[K in keyof Settings]?: (value: Settings[K]) => Promise<unknown>;
|
||||
} = {
|
||||
always_on_microphone: (value) =>
|
||||
invoke("update_microphone_mode", { alwaysOn: value }),
|
||||
commands.updateMicrophoneMode(value as boolean),
|
||||
audio_feedback: (value) =>
|
||||
invoke("change_audio_feedback_setting", { enabled: value }),
|
||||
commands.changeAudioFeedbackSetting(value as boolean),
|
||||
audio_feedback_volume: (value) =>
|
||||
invoke("change_audio_feedback_volume_setting", { volume: value }),
|
||||
commands.changeAudioFeedbackVolumeSetting(value as number),
|
||||
sound_theme: (value) =>
|
||||
invoke("change_sound_theme_setting", { theme: value }),
|
||||
commands.changeSoundThemeSetting(value as string),
|
||||
start_hidden: (value) =>
|
||||
invoke("change_start_hidden_setting", { enabled: value }),
|
||||
commands.changeStartHiddenSetting(value as boolean),
|
||||
autostart_enabled: (value) =>
|
||||
invoke("change_autostart_setting", { enabled: value }),
|
||||
push_to_talk: (value) => invoke("change_ptt_setting", { enabled: value }),
|
||||
commands.changeAutostartSetting(value as boolean),
|
||||
push_to_talk: (value) => commands.changePttSetting(value as boolean),
|
||||
selected_microphone: (value) =>
|
||||
invoke("set_selected_microphone", {
|
||||
deviceName: value === "Default" ? "default" : value,
|
||||
}),
|
||||
commands.setSelectedMicrophone(
|
||||
(value as string) === "Default" || value === null ? "default" : (value as string)
|
||||
),
|
||||
clamshell_microphone: (value) =>
|
||||
invoke("set_clamshell_microphone", {
|
||||
deviceName: value === "Default" ? "default" : value,
|
||||
}),
|
||||
commands.setClamshellMicrophone(
|
||||
(value as string) === "Default" ? "default" : (value as string)
|
||||
),
|
||||
selected_output_device: (value) =>
|
||||
invoke("set_selected_output_device", {
|
||||
deviceName: value === "Default" ? "default" : value,
|
||||
}),
|
||||
commands.setSelectedOutputDevice(
|
||||
(value as string) === "Default" || value === null ? "default" : (value as string)
|
||||
),
|
||||
recording_retention_period: (value) =>
|
||||
invoke("update_recording_retention_period", { period: value }),
|
||||
commands.updateRecordingRetentionPeriod(value as string),
|
||||
translate_to_english: (value) =>
|
||||
invoke("change_translate_to_english_setting", { enabled: value }),
|
||||
commands.changeTranslateToEnglishSetting(value as boolean),
|
||||
selected_language: (value) =>
|
||||
invoke("change_selected_language_setting", { language: value }),
|
||||
commands.changeSelectedLanguageSetting(value as string),
|
||||
overlay_position: (value) =>
|
||||
invoke("change_overlay_position_setting", { position: value }),
|
||||
commands.changeOverlayPositionSetting(value as string),
|
||||
debug_mode: (value) =>
|
||||
invoke("change_debug_mode_setting", { enabled: value }),
|
||||
custom_words: (value) => invoke("update_custom_words", { words: value }),
|
||||
commands.changeDebugModeSetting(value as boolean),
|
||||
custom_words: (value) => commands.updateCustomWords(value as string[]),
|
||||
word_correction_threshold: (value) =>
|
||||
invoke("change_word_correction_threshold_setting", { threshold: value }),
|
||||
commands.changeWordCorrectionThresholdSetting(value as number),
|
||||
paste_method: (value) =>
|
||||
invoke("change_paste_method_setting", { method: value }),
|
||||
commands.changePasteMethodSetting(value as string),
|
||||
clipboard_handling: (value) =>
|
||||
invoke("change_clipboard_handling_setting", { handling: value }),
|
||||
history_limit: (value) => invoke("update_history_limit", { limit: value }),
|
||||
commands.changeClipboardHandlingSetting(value as string),
|
||||
history_limit: (value) => commands.updateHistoryLimit(value as string),
|
||||
post_process_enabled: (value) =>
|
||||
invoke("change_post_process_enabled_setting", { enabled: value }),
|
||||
commands.changePostProcessEnabledSetting(value as boolean),
|
||||
post_process_selected_prompt_id: (value) =>
|
||||
invoke("set_post_process_selected_prompt", { id: value }),
|
||||
commands.setPostProcessSelectedPrompt(value as string),
|
||||
mute_while_recording: (value) =>
|
||||
invoke("change_mute_while_recording_setting", { enabled: value }),
|
||||
log_level: (value) => invoke("set_log_level", { level: value }),
|
||||
commands.changeMuteWhileRecordingSetting(value as boolean),
|
||||
log_level: (value) => commands.setLogLevel(value as any),
|
||||
};
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>()(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
settings: null,
|
||||
defaultSettings: null,
|
||||
isLoading: true,
|
||||
isUpdating: {},
|
||||
audioDevices: [],
|
||||
|
|
@ -152,6 +135,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
|
||||
// Internal setters
|
||||
setSettings: (settings) => set({ settings }),
|
||||
setDefaultSettings: (defaultSettings) => set({ defaultSettings }),
|
||||
setLoading: (isLoading) => set({ isLoading }),
|
||||
setUpdating: (key, updating) =>
|
||||
set((state) => ({
|
||||
|
|
@ -168,48 +152,24 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
// Load settings from store
|
||||
refreshSettings: async () => {
|
||||
try {
|
||||
const { load } = await import("@tauri-apps/plugin-store");
|
||||
const store = await load("settings_store.json", {
|
||||
defaults: DEFAULT_SETTINGS,
|
||||
autoSave: false,
|
||||
});
|
||||
const settings = (await store.get("settings")) as Settings;
|
||||
|
||||
// Load additional settings that come from invoke calls
|
||||
const [
|
||||
microphoneMode,
|
||||
selectedMicrophone,
|
||||
clamshellMicrophone,
|
||||
selectedOutputDevice,
|
||||
] = await Promise.allSettled([
|
||||
invoke("get_microphone_mode"),
|
||||
invoke("get_selected_microphone"),
|
||||
invoke("get_clamshell_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",
|
||||
clamshell_microphone:
|
||||
clamshellMicrophone.status === "fulfilled"
|
||||
? (clamshellMicrophone.value as string)
|
||||
: "Default",
|
||||
selected_output_device:
|
||||
selectedOutputDevice.status === "fulfilled"
|
||||
? (selectedOutputDevice.value as string)
|
||||
: "Default",
|
||||
};
|
||||
|
||||
set({ settings: mergedSettings, isLoading: false });
|
||||
const result = await commands.getAppSettings();
|
||||
if (result.status === "ok") {
|
||||
const settings = result.data;
|
||||
const normalizedSettings: Settings = {
|
||||
...settings,
|
||||
always_on_microphone: settings.always_on_microphone ?? false,
|
||||
selected_microphone:
|
||||
settings.selected_microphone ?? "Default",
|
||||
clamshell_microphone:
|
||||
settings.clamshell_microphone ?? "Default",
|
||||
selected_output_device:
|
||||
settings.selected_output_device ?? "Default",
|
||||
};
|
||||
set({ settings: normalizedSettings, isLoading: false });
|
||||
} else {
|
||||
console.error("Failed to load settings:", result.error);
|
||||
set({ isLoading: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load settings:", error);
|
||||
set({ isLoading: false });
|
||||
|
|
@ -219,16 +179,18 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
// Load audio devices
|
||||
refreshAudioDevices: async () => {
|
||||
try {
|
||||
const devices: AudioDevice[] = await invoke(
|
||||
"get_available_microphones",
|
||||
);
|
||||
const devicesWithDefault = [
|
||||
DEFAULT_AUDIO_DEVICE,
|
||||
...devices.filter(
|
||||
(d) => d.name !== "Default" && d.name !== "default",
|
||||
),
|
||||
];
|
||||
set({ audioDevices: devicesWithDefault });
|
||||
const result = await commands.getAvailableMicrophones();
|
||||
if (result.status === "ok") {
|
||||
const devicesWithDefault = [
|
||||
DEFAULT_AUDIO_DEVICE,
|
||||
...result.data.filter(
|
||||
(d) => d.name !== "Default" && d.name !== "default",
|
||||
),
|
||||
];
|
||||
set({ audioDevices: devicesWithDefault });
|
||||
} else {
|
||||
set({ audioDevices: [DEFAULT_AUDIO_DEVICE] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load audio devices:", error);
|
||||
set({ audioDevices: [DEFAULT_AUDIO_DEVICE] });
|
||||
|
|
@ -238,16 +200,18 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
// Load output devices
|
||||
refreshOutputDevices: async () => {
|
||||
try {
|
||||
const devices: AudioDevice[] = await invoke(
|
||||
"get_available_output_devices",
|
||||
);
|
||||
const devicesWithDefault = [
|
||||
DEFAULT_AUDIO_DEVICE,
|
||||
...devices.filter(
|
||||
(d) => d.name !== "Default" && d.name !== "default",
|
||||
),
|
||||
];
|
||||
set({ outputDevices: devicesWithDefault });
|
||||
const result = await commands.getAvailableOutputDevices();
|
||||
if (result.status === "ok") {
|
||||
const devicesWithDefault = [
|
||||
DEFAULT_AUDIO_DEVICE,
|
||||
...result.data.filter(
|
||||
(d) => d.name !== "Default" && d.name !== "default",
|
||||
),
|
||||
];
|
||||
set({ outputDevices: devicesWithDefault });
|
||||
} else {
|
||||
set({ outputDevices: [DEFAULT_AUDIO_DEVICE] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load output devices:", error);
|
||||
set({ outputDevices: [DEFAULT_AUDIO_DEVICE] });
|
||||
|
|
@ -257,7 +221,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
// Play a test sound
|
||||
playTestSound: async (soundType: "start" | "stop") => {
|
||||
try {
|
||||
await invoke("play_test_sound", { soundType });
|
||||
await commands.playTestSound(soundType);
|
||||
} catch (error) {
|
||||
console.error(`Failed to play test sound (${soundType}):`, error);
|
||||
}
|
||||
|
|
@ -265,8 +229,8 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
|
||||
checkCustomSounds: async () => {
|
||||
try {
|
||||
const sounds = await invoke("check_custom_sounds");
|
||||
get().setCustomSounds(sounds as { start: boolean; stop: boolean });
|
||||
const sounds = await commands.checkCustomSounds();
|
||||
get().setCustomSounds(sounds);
|
||||
} catch (error) {
|
||||
console.error("Failed to check custom sounds:", error);
|
||||
}
|
||||
|
|
@ -306,9 +270,12 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
|
||||
// Reset a setting to its default value
|
||||
resetSetting: async (key) => {
|
||||
const defaultValue = DEFAULT_SETTINGS[key];
|
||||
if (defaultValue !== undefined) {
|
||||
await get().updateSetting(key, defaultValue as any);
|
||||
const { defaultSettings } = get();
|
||||
if (defaultSettings) {
|
||||
const defaultValue = defaultSettings[key];
|
||||
if (defaultValue !== undefined) {
|
||||
await get().updateSetting(key, defaultValue as any);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -329,7 +296,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
bindings: {
|
||||
...state.settings.bindings,
|
||||
[id]: {
|
||||
...state.settings.bindings[id],
|
||||
...state.settings.bindings[id]!,
|
||||
current_binding: binding,
|
||||
},
|
||||
},
|
||||
|
|
@ -337,7 +304,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
: null,
|
||||
}));
|
||||
|
||||
await invoke("change_binding", { id, binding });
|
||||
await commands.changeBinding(id, binding);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update binding ${id}:`, error);
|
||||
|
||||
|
|
@ -350,7 +317,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
bindings: {
|
||||
...state.settings.bindings,
|
||||
[id]: {
|
||||
...state.settings.bindings[id],
|
||||
...state.settings.bindings[id]!,
|
||||
current_binding: originalBinding,
|
||||
},
|
||||
},
|
||||
|
|
@ -371,7 +338,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
setUpdating(updateKey, true);
|
||||
|
||||
try {
|
||||
await invoke("reset_binding", { id });
|
||||
await commands.resetBinding(id);
|
||||
await refreshSettings();
|
||||
} catch (error) {
|
||||
console.error(`Failed to reset binding ${id}:`, error);
|
||||
|
|
@ -396,7 +363,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
}
|
||||
|
||||
try {
|
||||
await invoke("set_post_process_provider", { providerId });
|
||||
await commands.setPostProcessProvider(providerId);
|
||||
await refreshSettings();
|
||||
} catch (error) {
|
||||
console.error("Failed to set post-process provider:", error);
|
||||
|
|
@ -421,27 +388,16 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
const { setUpdating, refreshSettings } = get();
|
||||
const updateKey = `post_process_${settingType}:${providerId}`;
|
||||
|
||||
// Map setting types to command names
|
||||
const commandMap = {
|
||||
base_url: "change_post_process_base_url_setting",
|
||||
api_key: "change_post_process_api_key_setting",
|
||||
model: "change_post_process_model_setting",
|
||||
};
|
||||
|
||||
// Map setting types to param names
|
||||
const paramMap = {
|
||||
base_url: "baseUrl",
|
||||
api_key: "apiKey",
|
||||
model: "model",
|
||||
};
|
||||
|
||||
setUpdating(updateKey, true);
|
||||
|
||||
try {
|
||||
await invoke(commandMap[settingType], {
|
||||
providerId,
|
||||
[paramMap[settingType]]: value,
|
||||
});
|
||||
if (settingType === "base_url") {
|
||||
await commands.changePostProcessBaseUrlSetting(providerId, value);
|
||||
} else if (settingType === "api_key") {
|
||||
await commands.changePostProcessApiKeySetting(providerId, value);
|
||||
} else if (settingType === "model") {
|
||||
await commands.changePostProcessModelSetting(providerId, value);
|
||||
}
|
||||
await refreshSettings();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
|
@ -480,12 +436,14 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
|
||||
try {
|
||||
// Call Tauri backend command instead of fetch
|
||||
const models: string[] = await invoke("fetch_post_process_models", {
|
||||
providerId,
|
||||
});
|
||||
|
||||
setPostProcessModelOptions(providerId, models);
|
||||
return models;
|
||||
const result = await commands.fetchPostProcessModels(providerId);
|
||||
if (result.status === "ok") {
|
||||
setPostProcessModelOptions(providerId, result.data);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error("Failed to fetch models:", result.error);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch models:", error);
|
||||
// Don't cache empty array on error - let user retry
|
||||
|
|
@ -503,6 +461,20 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
},
|
||||
})),
|
||||
|
||||
// Load default settings from Rust
|
||||
loadDefaultSettings: async () => {
|
||||
try {
|
||||
const result = await commands.getDefaultSettings();
|
||||
if (result.status === "ok") {
|
||||
set({ defaultSettings: result.data });
|
||||
} else {
|
||||
console.error("Failed to load default settings:", result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load default settings:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize everything
|
||||
initialize: async () => {
|
||||
const {
|
||||
|
|
@ -510,8 +482,10 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
refreshAudioDevices,
|
||||
refreshOutputDevices,
|
||||
checkCustomSounds,
|
||||
loadDefaultSettings,
|
||||
} = get();
|
||||
await Promise.all([
|
||||
loadDefaultSettings(),
|
||||
refreshSettings(),
|
||||
refreshAudioDevices(),
|
||||
refreshOutputDevices(),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/bindings": ["./src/bindings.ts"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ const host = process.env.TAURI_DEV_HOST;
|
|||
export default defineConfig(async () => ({
|
||||
plugins: [react(), tailwindcss()],
|
||||
|
||||
// Path aliases
|
||||
resolve: {
|
||||
alias: {
|
||||
"@/bindings": resolve(__dirname, "./src/bindings.ts"),
|
||||
},
|
||||
},
|
||||
|
||||
// Multiple entry points for main app and overlay
|
||||
build: {
|
||||
rollupOptions: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue