Add a cancel hotkey and configuration (#224)
* From scratch * Cleanup * Small fixes * simplify --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
8136e6ba1f
commit
381b8a8bfb
8 changed files with 191 additions and 77 deletions
|
|
@ -2,10 +2,10 @@ use crate::audio_feedback::{play_feedback_sound, play_feedback_sound_blocking, S
|
||||||
use crate::managers::audio::AudioRecordingManager;
|
use crate::managers::audio::AudioRecordingManager;
|
||||||
use crate::managers::history::HistoryManager;
|
use crate::managers::history::HistoryManager;
|
||||||
use crate::managers::transcription::TranscriptionManager;
|
use crate::managers::transcription::TranscriptionManager;
|
||||||
use crate::overlay::{show_recording_overlay, show_transcribing_overlay};
|
|
||||||
use crate::settings::{get_settings, AppSettings};
|
use crate::settings::{get_settings, AppSettings};
|
||||||
|
use crate::shortcut;
|
||||||
use crate::tray::{change_tray_icon, TrayIconState};
|
use crate::tray::{change_tray_icon, TrayIconState};
|
||||||
use crate::utils;
|
use crate::utils::{self, show_recording_overlay, show_transcribing_overlay};
|
||||||
use async_openai::types::{
|
use async_openai::types::{
|
||||||
ChatCompletionRequestMessage, ChatCompletionRequestUserMessageArgs,
|
ChatCompletionRequestMessage, ChatCompletionRequestUserMessageArgs,
|
||||||
CreateChatCompletionRequestArgs,
|
CreateChatCompletionRequestArgs,
|
||||||
|
|
@ -225,6 +225,7 @@ impl ShortcutAction for TranscribeAction {
|
||||||
let is_always_on = settings.always_on_microphone;
|
let is_always_on = settings.always_on_microphone;
|
||||||
debug!("Microphone mode - always_on: {}", is_always_on);
|
debug!("Microphone mode - always_on: {}", is_always_on);
|
||||||
|
|
||||||
|
let mut recording_started = false;
|
||||||
if is_always_on {
|
if is_always_on {
|
||||||
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
|
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
|
||||||
debug!("Always-on mode: Playing audio feedback immediately");
|
debug!("Always-on mode: Playing audio feedback immediately");
|
||||||
|
|
@ -237,7 +238,7 @@ impl ShortcutAction for TranscribeAction {
|
||||||
rm_clone.apply_mute();
|
rm_clone.apply_mute();
|
||||||
});
|
});
|
||||||
|
|
||||||
let recording_started = rm.try_start_recording(&binding_id);
|
recording_started = rm.try_start_recording(&binding_id);
|
||||||
debug!("Recording started: {}", recording_started);
|
debug!("Recording started: {}", recording_started);
|
||||||
} else {
|
} else {
|
||||||
// On-demand mode: Start recording first, then play audio feedback, then apply mute
|
// On-demand mode: Start recording first, then play audio feedback, then apply mute
|
||||||
|
|
@ -245,6 +246,7 @@ impl ShortcutAction for TranscribeAction {
|
||||||
debug!("On-demand mode: Starting recording first, then audio feedback");
|
debug!("On-demand mode: Starting recording first, then audio feedback");
|
||||||
let recording_start_time = Instant::now();
|
let recording_start_time = Instant::now();
|
||||||
if rm.try_start_recording(&binding_id) {
|
if rm.try_start_recording(&binding_id) {
|
||||||
|
recording_started = true;
|
||||||
debug!("Recording started in {:?}", recording_start_time.elapsed());
|
debug!("Recording started in {:?}", recording_start_time.elapsed());
|
||||||
// Small delay to ensure microphone stream is active
|
// Small delay to ensure microphone stream is active
|
||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
|
|
@ -262,6 +264,11 @@ impl ShortcutAction for TranscribeAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recording_started {
|
||||||
|
// Dynamically register the cancel shortcut in a separate task to avoid deadlock
|
||||||
|
shortcut::register_cancel_shortcut(app);
|
||||||
|
}
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"TranscribeAction::start completed in {:?}",
|
"TranscribeAction::start completed in {:?}",
|
||||||
start_time.elapsed()
|
start_time.elapsed()
|
||||||
|
|
@ -269,6 +276,9 @@ impl ShortcutAction for TranscribeAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&self, app: &AppHandle, binding_id: &str, _shortcut_str: &str) {
|
fn stop(&self, app: &AppHandle, binding_id: &str, _shortcut_str: &str) {
|
||||||
|
// Unregister the cancel shortcut when transcription stops
|
||||||
|
shortcut::unregister_cancel_shortcut(app);
|
||||||
|
|
||||||
let stop_time = Instant::now();
|
let stop_time = Instant::now();
|
||||||
debug!("TranscribeAction::stop called for binding: {}", binding_id);
|
debug!("TranscribeAction::stop called for binding: {}", binding_id);
|
||||||
|
|
||||||
|
|
@ -406,6 +416,19 @@ impl ShortcutAction for TranscribeAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel Action
|
||||||
|
struct CancelAction;
|
||||||
|
|
||||||
|
impl ShortcutAction for CancelAction {
|
||||||
|
fn start(&self, app: &AppHandle, _binding_id: &str, _shortcut_str: &str) {
|
||||||
|
utils::cancel_current_operation(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, _app: &AppHandle, _binding_id: &str, _shortcut_str: &str) {
|
||||||
|
// Nothing to do on stop for cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test Action
|
// Test Action
|
||||||
struct TestAction;
|
struct TestAction;
|
||||||
|
|
||||||
|
|
@ -436,6 +459,10 @@ pub static ACTION_MAP: Lazy<HashMap<String, Arc<dyn ShortcutAction>>> = Lazy::ne
|
||||||
"transcribe".to_string(),
|
"transcribe".to_string(),
|
||||||
Arc::new(TranscribeAction) as Arc<dyn ShortcutAction>,
|
Arc::new(TranscribeAction) as Arc<dyn ShortcutAction>,
|
||||||
);
|
);
|
||||||
|
map.insert(
|
||||||
|
"cancel".to_string(),
|
||||||
|
Arc::new(CancelAction) as Arc<dyn ShortcutAction>,
|
||||||
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
"test".to_string(),
|
"test".to_string(),
|
||||||
Arc::new(TestAction) as Arc<dyn ShortcutAction>,
|
Arc::new(TestAction) as Arc<dyn ShortcutAction>,
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,12 @@ impl AudioRecordingManager {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn is_recording(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
*self.state.lock().unwrap(),
|
||||||
|
RecordingState::Recording { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancel any ongoing recording without returning audio samples
|
/// Cancel any ongoing recording without returning audio samples
|
||||||
pub fn cancel_recording(&self) {
|
pub fn cancel_recording(&self) {
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,16 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
current_binding: default_shortcut.to_string(),
|
current_binding: default_shortcut.to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
bindings.insert(
|
||||||
|
"cancel".to_string(),
|
||||||
|
ShortcutBinding {
|
||||||
|
id: "cancel".to_string(),
|
||||||
|
name: "Cancel".to_string(),
|
||||||
|
description: "Cancels the current recording.".to_string(),
|
||||||
|
default_binding: "escape".to_string(),
|
||||||
|
current_binding: "escape".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
AppSettings {
|
AppSettings {
|
||||||
bindings,
|
bindings,
|
||||||
|
|
@ -501,8 +511,25 @@ pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings {
|
||||||
let settings = if let Some(settings_value) = store.get("settings") {
|
let settings = if let Some(settings_value) = store.get("settings") {
|
||||||
// Parse the entire settings object
|
// Parse the entire settings object
|
||||||
match serde_json::from_value::<AppSettings>(settings_value) {
|
match serde_json::from_value::<AppSettings>(settings_value) {
|
||||||
Ok(settings) => {
|
Ok(mut settings) => {
|
||||||
debug!("Found existing settings: {:?}", settings);
|
debug!("Found existing settings: {:?}", settings);
|
||||||
|
let default_settings = get_default_settings();
|
||||||
|
let mut updated = false;
|
||||||
|
|
||||||
|
// Merge default bindings into existing settings
|
||||||
|
for (key, value) in default_settings.bindings {
|
||||||
|
if !settings.bindings.contains_key(&key) {
|
||||||
|
debug!("Adding missing binding: {}", key);
|
||||||
|
settings.bindings.insert(key, value);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated {
|
||||||
|
debug!("Settings updated with new bindings");
|
||||||
|
store.set("settings", serde_json::to_value(&settings).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||||
|
|
||||||
use crate::actions::ACTION_MAP;
|
use crate::actions::ACTION_MAP;
|
||||||
|
use crate::managers::audio::AudioRecordingManager;
|
||||||
use crate::settings::ShortcutBinding;
|
use crate::settings::ShortcutBinding;
|
||||||
use crate::settings::{
|
use crate::settings::{
|
||||||
self, get_settings, ClipboardHandling, LLMPrompt, OverlayPosition, PasteMethod, SoundTheme,
|
self, get_settings, ClipboardHandling, LLMPrompt, OverlayPosition, PasteMethod, SoundTheme,
|
||||||
|
|
@ -13,12 +15,23 @@ use crate::settings::{
|
||||||
use crate::ManagedToggleState;
|
use crate::ManagedToggleState;
|
||||||
|
|
||||||
pub fn init_shortcuts(app: &AppHandle) {
|
pub fn init_shortcuts(app: &AppHandle) {
|
||||||
let settings = settings::load_or_create_app_settings(app);
|
let default_bindings = settings::get_default_settings().bindings;
|
||||||
|
let user_settings = settings::load_or_create_app_settings(app);
|
||||||
|
|
||||||
// Register shortcuts with the bindings from settings
|
|
||||||
for (_id, binding) in settings.bindings {
|
// Register all default shortcuts, applying user customizations
|
||||||
if let Err(e) = _register_shortcut(app, binding) {
|
for (id, default_binding) in default_bindings {
|
||||||
error!("Failed to register shortcut {} during init: {}", _id, e);
|
if id == "cancel" {
|
||||||
|
continue; // Skip cancel shortcut, it will be registered dynamically
|
||||||
|
}
|
||||||
|
let binding = user_settings
|
||||||
|
.bindings
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(default_binding);
|
||||||
|
|
||||||
|
if let Err(e) = register_shortcut(app, binding) {
|
||||||
|
error!("Failed to register shortcut {} during init: {}", id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,9 +65,23 @@ pub fn change_binding(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// If this is the cancel binding, just update the settings and return
|
||||||
|
// It's managed dynamically, so we don't register/unregister here
|
||||||
|
if id == "cancel" {
|
||||||
|
if let Some(mut b) = settings.bindings.get(&id).cloned() {
|
||||||
|
b.current_binding = binding;
|
||||||
|
settings.bindings.insert(id.clone(), b.clone());
|
||||||
|
settings::write_settings(&app, settings);
|
||||||
|
return Ok(BindingResponse {
|
||||||
|
success: true,
|
||||||
|
binding: Some(b.clone()),
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unregister the existing binding
|
// Unregister the existing binding
|
||||||
if let Err(e) = _unregister_shortcut(&app, binding_to_modify.clone()) {
|
if let Err(e) = unregister_shortcut(&app, binding_to_modify.clone()) {
|
||||||
let error_msg = format!("Failed to unregister shortcut: {}", e);
|
let error_msg = format!("Failed to unregister shortcut: {}", e);
|
||||||
error!("change_binding error: {}", error_msg);
|
error!("change_binding error: {}", error_msg);
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +97,7 @@ pub fn change_binding(
|
||||||
updated_binding.current_binding = binding;
|
updated_binding.current_binding = binding;
|
||||||
|
|
||||||
// Register the new binding
|
// Register the new binding
|
||||||
if let Err(e) = _register_shortcut(&app, updated_binding.clone()) {
|
if let Err(e) = register_shortcut(&app, updated_binding.clone()) {
|
||||||
let error_msg = format!("Failed to register shortcut: {}", e);
|
let error_msg = format!("Failed to register shortcut: {}", e);
|
||||||
error!("change_binding error: {}", error_msg);
|
error!("change_binding error: {}", error_msg);
|
||||||
return Ok(BindingResponse {
|
return Ok(BindingResponse {
|
||||||
|
|
@ -673,7 +700,7 @@ fn validate_shortcut_string(raw: &str) -> Result<(), String> {
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
|
pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
|
||||||
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
||||||
if let Err(e) = _unregister_shortcut(&app, b) {
|
if let Err(e) = unregister_shortcut(&app, b) {
|
||||||
error!("suspend_binding error for id '{}': {}", id, e);
|
error!("suspend_binding error for id '{}': {}", id, e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
@ -686,7 +713,7 @@ pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
|
pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
|
||||||
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
||||||
if let Err(e) = _register_shortcut(&app, b) {
|
if let Err(e) = register_shortcut(&app, b) {
|
||||||
error!("resume_binding error for id '{}': {}", id, e);
|
error!("resume_binding error for id '{}': {}", id, e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
@ -694,7 +721,28 @@ pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
pub fn register_cancel_shortcut(app: &AppHandle) {
|
||||||
|
let app_clone = app.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Some(cancel_binding) = get_settings(&app_clone).bindings.get("cancel").cloned() {
|
||||||
|
if let Err(e) = register_shortcut(&app_clone, cancel_binding) {
|
||||||
|
eprintln!("Failed to register cancel shortcut: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_cancel_shortcut(app: &AppHandle) {
|
||||||
|
let app_clone = app.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Some(cancel_binding) = get_settings(&app_clone).bindings.get("cancel").cloned() {
|
||||||
|
// We ignore errors here as it might already be unregistered
|
||||||
|
let _ = unregister_shortcut(&app_clone, cancel_binding);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
||||||
// Validate human-level rules first
|
// Validate human-level rules first
|
||||||
if let Err(e) = validate_shortcut_string(&binding.current_binding) {
|
if let Err(e) = validate_shortcut_string(&binding.current_binding) {
|
||||||
warn!(
|
warn!(
|
||||||
|
|
@ -734,7 +782,13 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
|
||||||
let settings = get_settings(ah);
|
let settings = get_settings(ah);
|
||||||
|
|
||||||
if let Some(action) = ACTION_MAP.get(&binding_id_for_closure) {
|
if let Some(action) = ACTION_MAP.get(&binding_id_for_closure) {
|
||||||
if settings.push_to_talk {
|
if binding_id_for_closure == "cancel" {
|
||||||
|
let audio_manager = ah.state::<Arc<AudioRecordingManager>>();
|
||||||
|
if audio_manager.is_recording() && event.state == ShortcutState::Pressed {
|
||||||
|
action.start(ah, &binding_id_for_closure, &shortcut_string);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if settings.push_to_talk {
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
action.start(ah, &binding_id_for_closure, &shortcut_string);
|
action.start(ah, &binding_id_for_closure, &shortcut_string);
|
||||||
} else if event.state == ShortcutState::Released {
|
} else if event.state == ShortcutState::Released {
|
||||||
|
|
@ -780,7 +834,7 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
pub fn unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
||||||
let shortcut = match binding.current_binding.parse::<Shortcut>() {
|
let shortcut = match binding.current_binding.parse::<Shortcut>() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::actions::ACTION_MAP;
|
|
||||||
use crate::managers::audio::AudioRecordingManager;
|
use crate::managers::audio::AudioRecordingManager;
|
||||||
|
use crate::shortcut;
|
||||||
use crate::ManagedToggleState;
|
use crate::ManagedToggleState;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -16,31 +16,14 @@ pub use crate::tray::*;
|
||||||
pub fn cancel_current_operation(app: &AppHandle) {
|
pub fn cancel_current_operation(app: &AppHandle) {
|
||||||
info!("Initiating operation cancellation...");
|
info!("Initiating operation cancellation...");
|
||||||
|
|
||||||
// First, reset all shortcut toggle states and call stop actions
|
// Unregister the cancel shortcut asynchronously
|
||||||
|
shortcut::unregister_cancel_shortcut(app);
|
||||||
|
|
||||||
|
// First, reset all shortcut toggle states.
|
||||||
// This is critical for non-push-to-talk mode where shortcuts toggle on/off
|
// This is critical for non-push-to-talk mode where shortcuts toggle on/off
|
||||||
let toggle_state_manager = app.state::<ManagedToggleState>();
|
let toggle_state_manager = app.state::<ManagedToggleState>();
|
||||||
if let Ok(mut states) = toggle_state_manager.lock() {
|
if let Ok(mut states) = toggle_state_manager.lock() {
|
||||||
// For each currently active toggle, call its stop action and reset state
|
states.active_toggles.values_mut().for_each(|v| *v = false);
|
||||||
let active_bindings: Vec<String> = states
|
|
||||||
.active_toggles
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, &is_active)| is_active)
|
|
||||||
.map(|(binding_id, _)| binding_id.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for binding_id in active_bindings {
|
|
||||||
info!("Stopping active action for binding: {}", binding_id);
|
|
||||||
|
|
||||||
// Call the action's stop method to ensure proper cleanup
|
|
||||||
if let Some(action) = ACTION_MAP.get(&binding_id) {
|
|
||||||
action.stop(app, &binding_id, "cancelled");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the toggle state
|
|
||||||
if let Some(is_active) = states.active_toggles.get_mut(&binding_id) {
|
|
||||||
*is_active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
warn!("Failed to lock toggle state manager during cancellation");
|
warn!("Failed to lock toggle state manager during cancellation");
|
||||||
}
|
}
|
||||||
|
|
@ -49,8 +32,9 @@ pub fn cancel_current_operation(app: &AppHandle) {
|
||||||
let audio_manager = app.state::<Arc<AudioRecordingManager>>();
|
let audio_manager = app.state::<Arc<AudioRecordingManager>>();
|
||||||
audio_manager.cancel_recording();
|
audio_manager.cancel_recording();
|
||||||
|
|
||||||
// Update tray icon and menu to idle state
|
// Update tray icon and hide overlay
|
||||||
change_tray_icon(app, crate::tray::TrayIconState::Idle);
|
change_tray_icon(app, crate::tray::TrayIconState::Idle);
|
||||||
|
hide_recording_overlay(app);
|
||||||
|
|
||||||
info!("Operation cancellation completed - returned to idle state");
|
info!("Operation cancellation completed - returned to idle state");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,15 @@ import { toast } from "sonner";
|
||||||
interface HandyShortcutProps {
|
interface HandyShortcutProps {
|
||||||
descriptionMode?: "inline" | "tooltip";
|
descriptionMode?: "inline" | "tooltip";
|
||||||
grouped?: boolean;
|
grouped?: boolean;
|
||||||
|
shortcutId: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
||||||
descriptionMode = "tooltip",
|
descriptionMode = "tooltip",
|
||||||
grouped = false,
|
grouped = false,
|
||||||
|
shortcutId,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { getSetting, updateBinding, resetBinding, isUpdating, isLoading } =
|
const { getSetting, updateBinding, resetBinding, isUpdating, isLoading } =
|
||||||
useSettings();
|
useSettings();
|
||||||
|
|
@ -267,48 +271,50 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const binding = bindings[shortcutId];
|
||||||
|
if (!binding) {
|
||||||
|
return (
|
||||||
|
<SettingContainer
|
||||||
|
title="Shortcut"
|
||||||
|
description="Shortcut not found"
|
||||||
|
descriptionMode={descriptionMode}
|
||||||
|
grouped={grouped}
|
||||||
|
>
|
||||||
|
<div className="text-sm text-mid-gray">No shortcut configured</div>
|
||||||
|
</SettingContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer
|
<SettingContainer
|
||||||
title="Handy Shortcut"
|
title={binding.name}
|
||||||
description="Set the keyboard shortcut to start and stop speech-to-text recording"
|
description={binding.description}
|
||||||
descriptionMode={descriptionMode}
|
descriptionMode={descriptionMode}
|
||||||
grouped={grouped}
|
grouped={grouped}
|
||||||
tooltipPosition="bottom"
|
disabled={disabled}
|
||||||
|
layout="horizontal"
|
||||||
>
|
>
|
||||||
{(() => {
|
<div className="flex items-center space-x-1">
|
||||||
const primaryBinding = Object.values(bindings)[0];
|
{editingShortcutId === shortcutId ? (
|
||||||
const primaryId = Object.keys(bindings)[0];
|
<div
|
||||||
|
ref={(ref) => setShortcutRef(shortcutId, ref)}
|
||||||
if (!primaryBinding) {
|
className="px-2 py-1 text-sm font-semibold border border-logo-primary bg-logo-primary/30 rounded min-w-[120px] text-center"
|
||||||
return (
|
>
|
||||||
<div className="text-sm text-mid-gray">No shortcuts configured</div>
|
{formatCurrentKeys()}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{editingShortcutId === primaryId ? (
|
|
||||||
<div
|
|
||||||
ref={(ref) => setShortcutRef(primaryId, ref)}
|
|
||||||
className="px-2 py-1 text-sm font-semibold border border-logo-primary bg-logo-primary/30 rounded min-w-[120px] text-center"
|
|
||||||
>
|
|
||||||
{formatCurrentKeys()}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 hover:bg-logo-primary/10 rounded cursor-pointer hover:border-logo-primary"
|
|
||||||
onClick={() => startRecording(primaryId)}
|
|
||||||
>
|
|
||||||
{formatKeyCombination(primaryBinding.current_binding, osType)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ResetButton
|
|
||||||
onClick={() => resetBinding(primaryId)}
|
|
||||||
disabled={isUpdating(`binding_${primaryId}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
) : (
|
||||||
})()}
|
<div
|
||||||
|
className="px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 hover:bg-logo-primary/10 rounded cursor-pointer hover:border-logo-primary"
|
||||||
|
onClick={() => startRecording(shortcutId)}
|
||||||
|
>
|
||||||
|
{formatKeyCombination(binding.current_binding, osType)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ResetButton
|
||||||
|
onClick={() => resetBinding(shortcutId)}
|
||||||
|
disabled={isUpdating(`binding_${shortcutId}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,13 @@ import { PostProcessingToggle } from "../PostProcessingToggle";
|
||||||
import { MuteWhileRecording } from "../MuteWhileRecording";
|
import { MuteWhileRecording } from "../MuteWhileRecording";
|
||||||
import { RecordingRetentionPeriodSelector } from "../RecordingRetentionPeriod";
|
import { RecordingRetentionPeriodSelector } from "../RecordingRetentionPeriod";
|
||||||
import { ClamshellMicrophoneSelector } from "../ClamshellMicrophoneSelector";
|
import { ClamshellMicrophoneSelector } from "../ClamshellMicrophoneSelector";
|
||||||
|
import { HandyShortcut } from "../HandyShortcut";
|
||||||
|
import { useSettings } from "../../../hooks/useSettings";
|
||||||
|
|
||||||
export const DebugSettings: React.FC = () => {
|
export const DebugSettings: React.FC = () => {
|
||||||
|
const { getSetting } = useSettings();
|
||||||
|
const pushToTalk = getSetting("push_to_talk");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||||
<SettingsGroup title="Debug">
|
<SettingsGroup title="Debug">
|
||||||
|
|
@ -31,6 +36,11 @@ export const DebugSettings: React.FC = () => {
|
||||||
<ClamshellMicrophoneSelector descriptionMode="tooltip" grouped={true} />
|
<ClamshellMicrophoneSelector descriptionMode="tooltip" grouped={true} />
|
||||||
<PostProcessingToggle descriptionMode="tooltip" grouped={true} />
|
<PostProcessingToggle descriptionMode="tooltip" grouped={true} />
|
||||||
<MuteWhileRecording descriptionMode="tooltip" grouped={true} />
|
<MuteWhileRecording descriptionMode="tooltip" grouped={true} />
|
||||||
|
<HandyShortcut
|
||||||
|
shortcutId="cancel"
|
||||||
|
grouped={true}
|
||||||
|
disabled={pushToTalk}
|
||||||
|
/>
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const GeneralSettings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||||
<SettingsGroup title="General">
|
<SettingsGroup title="General">
|
||||||
<HandyShortcut descriptionMode="tooltip" grouped={true} />
|
<HandyShortcut shortcutId="transcribe" grouped={true} />
|
||||||
<LanguageSelector descriptionMode="tooltip" grouped={true} />
|
<LanguageSelector descriptionMode="tooltip" grouped={true} />
|
||||||
<PushToTalk descriptionMode="tooltip" grouped={true} />
|
<PushToTalk descriptionMode="tooltip" grouped={true} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue