feat(tray): add cancel recording/transcribing actions

This commit is contained in:
Vlad Gerasimov 2025-07-27 22:46:59 +04:00
parent e92f94573e
commit 6c26d6afb8
3 changed files with 108 additions and 0 deletions

View file

@ -122,6 +122,25 @@ pub fn run() {
"check_updates" => {
let _ = app.emit("check-for-updates", ());
}
"cancel_listening" => {
use crate::utils::{change_tray_icon, TrayIconState};
use crate::managers::audio::AudioRecordingManager;
use std::sync::Arc;
// Cancel the recording without transcribing
let rm = app.state::<Arc<AudioRecordingManager>>();
rm.cancel_recording();
// Return to idle state
change_tray_icon(app, TrayIconState::Idle);
}
"cancel_transcribing" => {
use crate::utils::{change_tray_icon, TrayIconState};
// For now, just return to idle state
// TODO: In a future version, we could add actual request cancellation
change_tray_icon(app, TrayIconState::Idle);
}
"quit" => {
app.exit(0);
}

View file

@ -258,4 +258,25 @@ impl AudioRecordingManager {
_ => None,
}
}
/// Cancel any ongoing recording without returning audio samples
pub fn cancel_recording(&self) {
let mut state = self.state.lock().unwrap();
if let RecordingState::Recording { .. } = *state {
*state = RecordingState::Idle;
drop(state);
if let Some(rec) = self.recorder.lock().unwrap().as_ref() {
let _ = rec.stop(); // Discard the result
}
*self.is_recording.lock().unwrap() = false;
// In on-demand mode turn the mic off again
if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) {
self.stop_microphone_stream();
}
}
}
}

View file

@ -14,6 +14,7 @@ use tauri::image::Image;
use tauri::tray::TrayIcon;
use tauri::AppHandle;
use tauri::Manager;
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri_plugin_clipboard_manager::ClipboardExt;
fn send_paste() -> Result<(), String> {
@ -70,6 +71,7 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
Ok(())
}
#[derive(Clone, Debug, PartialEq)]
pub enum TrayIconState {
Idle,
Recording,
@ -93,6 +95,72 @@ pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) {
)
.expect("failed to set icon"),
));
// Update menu based on state
update_tray_menu(app, &icon);
}
pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState) {
let version = env!("CARGO_PKG_VERSION");
let version_label = format!("Handy v{}", version);
// Platform-specific accelerators
#[cfg(target_os = "macos")]
let settings_accelerator = Some("Cmd+,");
#[cfg(not(target_os = "macos"))]
let settings_accelerator = Some("Ctrl+,");
#[cfg(target_os = "macos")]
let quit_accelerator = Some("Cmd+Q");
#[cfg(not(target_os = "macos"))]
let quit_accelerator = Some("Ctrl+Q");
let version_i = MenuItem::with_id(app, "version", &version_label, false, None::<&str>).expect("failed to create version item");
let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, settings_accelerator).expect("failed to create settings item");
let check_updates_i = MenuItem::with_id(app, "check_updates", "Check for Updates...", true, None::<&str>).expect("failed to create check updates item");
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, quit_accelerator).expect("failed to create quit item");
let menu = match state {
TrayIconState::Recording => {
let cancel_i = MenuItem::with_id(app, "cancel_listening", "Cancel Listening", true, None::<&str>).expect("failed to create cancel listening item");
Menu::with_items(app, &[
&version_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&cancel_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&settings_i,
&check_updates_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&quit_i,
]).expect("failed to create menu")
}
TrayIconState::Transcribing => {
let cancel_i = MenuItem::with_id(app, "cancel_transcribing", "Cancel Transcribing", true, None::<&str>).expect("failed to create cancel transcribing item");
Menu::with_items(app, &[
&version_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&cancel_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&settings_i,
&check_updates_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&quit_i,
]).expect("failed to create menu")
}
TrayIconState::Idle => {
Menu::with_items(app, &[
&version_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&settings_i,
&check_updates_i,
&PredefinedMenuItem::separator(app).expect("failed to create separator"),
&quit_i,
]).expect("failed to create menu")
}
};
let tray = app.state::<TrayIcon>();
let _ = tray.set_menu(Some(menu));
}
/// Plays an audio resource from the resources directory.