From 6522944886cf2e34321062bdc4e3bb01aa5f71a5 Mon Sep 17 00:00:00 2001 From: ja <51257127+anntnzrb@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:33:50 -0500 Subject: [PATCH] feat(tray): add copy last transcript action (#598) --- src-tauri/src/lib.rs | 3 + src-tauri/src/managers/history.rs | 94 ++++++++++++++++++++++++++++ src-tauri/src/tray.rs | 76 ++++++++++++++++++++++ src/i18n/locales/cs/translation.json | 1 + src/i18n/locales/de/translation.json | 1 + src/i18n/locales/en/translation.json | 1 + src/i18n/locales/es/translation.json | 1 + src/i18n/locales/fr/translation.json | 1 + src/i18n/locales/it/translation.json | 1 + src/i18n/locales/ja/translation.json | 1 + src/i18n/locales/pl/translation.json | 1 + src/i18n/locales/pt/translation.json | 1 + src/i18n/locales/ru/translation.json | 1 + src/i18n/locales/tr/translation.json | 1 + src/i18n/locales/uk/translation.json | 1 + src/i18n/locales/vi/translation.json | 1 + src/i18n/locales/zh/translation.json | 1 + 17 files changed, 187 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5ecd91a..537b3e2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -180,6 +180,9 @@ fn initialize_core_logic(app_handle: &AppHandle) { let _ = app.emit("check-for-updates", ()); } } + "copy_last_transcript" => { + tray::copy_last_transcript(app); + } "cancel" => { use crate::utils::cancel_current_operation; diff --git a/src-tauri/src/managers/history.rs b/src-tauri/src/managers/history.rs index 0a8c7f0..cd5ee26 100644 --- a/src-tauri/src/managers/history.rs +++ b/src-tauri/src/managers/history.rs @@ -379,6 +379,37 @@ impl HistoryManager { Ok(entries) } + pub fn get_latest_entry(&self) -> Result> { + let conn = self.get_connection()?; + Self::get_latest_entry_with_conn(&conn) + } + + fn get_latest_entry_with_conn(conn: &Connection) -> Result> { + let mut stmt = conn.prepare( + "SELECT id, file_name, timestamp, saved, title, transcription_text, post_processed_text, post_process_prompt + FROM transcription_history + ORDER BY timestamp DESC + LIMIT 1", + )?; + + let entry = stmt + .query_row([], |row| { + Ok(HistoryEntry { + id: row.get("id")?, + file_name: row.get("file_name")?, + timestamp: row.get("timestamp")?, + saved: row.get("saved")?, + title: row.get("title")?, + transcription_text: row.get("transcription_text")?, + post_processed_text: row.get("post_processed_text")?, + post_process_prompt: row.get("post_process_prompt")?, + }) + }) + .optional()?; + + Ok(entry) + } + pub async fn toggle_saved_status(&self, id: i64) -> Result<()> { let conn = self.get_connection()?; @@ -476,3 +507,66 @@ impl HistoryManager { } } } + +#[cfg(test)] +mod tests { + use super::*; + use rusqlite::{params, Connection}; + + fn setup_conn() -> Connection { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch( + "CREATE TABLE transcription_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_name TEXT NOT NULL, + timestamp INTEGER NOT NULL, + saved BOOLEAN NOT NULL DEFAULT 0, + title TEXT NOT NULL, + transcription_text TEXT NOT NULL, + post_processed_text TEXT, + post_process_prompt TEXT + );", + ) + .expect("create transcription_history table"); + conn + } + + fn insert_entry(conn: &Connection, timestamp: i64, text: &str, post_processed: Option<&str>) { + conn.execute( + "INSERT INTO transcription_history (file_name, timestamp, saved, title, transcription_text, post_processed_text, post_process_prompt) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + format!("handy-{}.wav", timestamp), + timestamp, + false, + format!("Recording {}", timestamp), + text, + post_processed, + Option::::None + ], + ) + .expect("insert history entry"); + } + + #[test] + fn get_latest_entry_returns_none_when_empty() { + let conn = setup_conn(); + let entry = HistoryManager::get_latest_entry_with_conn(&conn).expect("fetch latest entry"); + assert!(entry.is_none()); + } + + #[test] + fn get_latest_entry_returns_newest_entry() { + let conn = setup_conn(); + insert_entry(&conn, 100, "first", None); + insert_entry(&conn, 200, "second", Some("processed")); + + let entry = HistoryManager::get_latest_entry_with_conn(&conn) + .expect("fetch latest entry") + .expect("entry exists"); + + assert_eq!(entry.timestamp, 200); + assert_eq!(entry.transcription_text, "second"); + assert_eq!(entry.post_processed_text.as_deref(), Some("processed")); + } +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index b77a623..3cdffb7 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1,9 +1,13 @@ +use crate::managers::history::{HistoryEntry, HistoryManager}; use crate::settings; use crate::tray_i18n::get_tray_translations; +use log::{error, info, warn}; +use std::sync::Arc; use tauri::image::Image; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIcon; use tauri::{AppHandle, Manager, Theme}; +use tauri_plugin_clipboard_manager::ClipboardExt; #[derive(Clone, Debug, PartialEq)] pub enum TrayIconState { @@ -111,6 +115,14 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<& None::<&str>, ) .expect("failed to create check updates item"); + let copy_last_transcript_i = MenuItem::with_id( + app, + "copy_last_transcript", + &strings.copy_last_transcript, + true, + None::<&str>, + ) + .expect("failed to create copy last transcript item"); let quit_i = MenuItem::with_id(app, "quit", &strings.quit, true, quit_accelerator) .expect("failed to create quit item"); let separator = || PredefinedMenuItem::separator(app).expect("failed to create separator"); @@ -126,6 +138,8 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<& &separator(), &cancel_i, &separator(), + ©_last_transcript_i, + &separator(), &settings_i, &check_updates_i, &separator(), @@ -139,6 +153,8 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<& &[ &version_i, &separator(), + ©_last_transcript_i, + &separator(), &settings_i, &check_updates_i, &separator(), @@ -152,3 +168,63 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<& let _ = tray.set_menu(Some(menu)); let _ = tray.set_icon_as_template(true); } + +fn last_transcript_text(entry: &HistoryEntry) -> &str { + entry + .post_processed_text + .as_deref() + .unwrap_or(&entry.transcription_text) +} + +pub fn copy_last_transcript(app: &AppHandle) { + let history_manager = app.state::>(); + let entry = match history_manager.get_latest_entry() { + Ok(Some(entry)) => entry, + Ok(None) => { + warn!("No transcription history entries available for tray copy."); + return; + } + Err(err) => { + error!("Failed to fetch last transcription entry: {}", err); + return; + } + }; + + if let Err(err) = app.clipboard().write_text(last_transcript_text(&entry)) { + error!("Failed to copy last transcript to clipboard: {}", err); + return; + } + + info!("Copied last transcript to clipboard via tray."); +} + +#[cfg(test)] +mod tests { + use super::last_transcript_text; + use crate::managers::history::HistoryEntry; + + fn build_entry(transcription: &str, post_processed: Option<&str>) -> HistoryEntry { + HistoryEntry { + id: 1, + file_name: "handy-1.wav".to_string(), + timestamp: 0, + saved: false, + title: "Recording".to_string(), + transcription_text: transcription.to_string(), + post_processed_text: post_processed.map(|text| text.to_string()), + post_process_prompt: None, + } + } + + #[test] + fn uses_post_processed_text_when_available() { + let entry = build_entry("raw", Some("processed")); + assert_eq!(last_transcript_text(&entry), "processed"); + } + + #[test] + fn falls_back_to_raw_transcription() { + let entry = build_entry("raw", None); + assert_eq!(last_transcript_text(&entry), "raw"); + } +} diff --git a/src/i18n/locales/cs/translation.json b/src/i18n/locales/cs/translation.json index 2067baa..142b531 100644 --- a/src/i18n/locales/cs/translation.json +++ b/src/i18n/locales/cs/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Nastavení...", "checkUpdates": "Zkontrolovat aktualizace...", + "copyLastTranscript": "Zkopírovat poslední přepis", "quit": "Ukončit", "cancel": "Zrušit" }, diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 10d6c2e..64ccbeb 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Einstellungen...", "checkUpdates": "Nach Updates suchen...", + "copyLastTranscript": "Letzte Transkription kopieren", "quit": "Beenden", "cancel": "Abbrechen" }, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 70ecd49..1b8a780 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Settings...", "checkUpdates": "Check for Updates...", + "copyLastTranscript": "Copy Last Transcript", "quit": "Quit", "cancel": "Cancel" }, diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index ec2d939..b4ddcf6 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Configuración...", "checkUpdates": "Buscar actualizaciones...", + "copyLastTranscript": "Copiar la última transcripción", "quit": "Salir", "cancel": "Cancelar" }, diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index c3416a0..d0e95d0 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -3,6 +3,7 @@ "tray": { "settings": "Paramètres...", "checkUpdates": "Rechercher des mises à jour...", + "copyLastTranscript": "Copier la dernière transcription", "quit": "Quitter", "cancel": "Annuler" }, diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index fae468c..dbfa7d9 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Impostazioni...", "checkUpdates": "Verifica aggiornamenti...", + "copyLastTranscript": "Copia l'ultima trascrizione", "quit": "Esci", "cancel": "Annulla" }, diff --git a/src/i18n/locales/ja/translation.json b/src/i18n/locales/ja/translation.json index 60b7d6e..b320a14 100644 --- a/src/i18n/locales/ja/translation.json +++ b/src/i18n/locales/ja/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "設定...", "checkUpdates": "アップデートを確認...", + "copyLastTranscript": "最新の文字起こしをコピー", "quit": "終了", "cancel": "キャンセル" }, diff --git a/src/i18n/locales/pl/translation.json b/src/i18n/locales/pl/translation.json index a5cefd5..3c69902 100644 --- a/src/i18n/locales/pl/translation.json +++ b/src/i18n/locales/pl/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Ustawienia...", "checkUpdates": "Sprawdź aktualizacje...", + "copyLastTranscript": "Kopiuj ostatnią transkrypcję", "quit": "Zamknij", "cancel": "Anuluj" }, diff --git a/src/i18n/locales/pt/translation.json b/src/i18n/locales/pt/translation.json index 1b893cd..ddc53fe 100644 --- a/src/i18n/locales/pt/translation.json +++ b/src/i18n/locales/pt/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Configurações...", "checkUpdates": "Verificar Atualizações...", + "copyLastTranscript": "Copiar última transcrição", "quit": "Sair", "cancel": "Cancelar" }, diff --git a/src/i18n/locales/ru/translation.json b/src/i18n/locales/ru/translation.json index ccc865d..15c9782 100644 --- a/src/i18n/locales/ru/translation.json +++ b/src/i18n/locales/ru/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Настройки...", "checkUpdates": "Проверить обновления...", + "copyLastTranscript": "Скопировать последнюю транскрипцию", "quit": "Выход", "cancel": "Отмена" }, diff --git a/src/i18n/locales/tr/translation.json b/src/i18n/locales/tr/translation.json index de40c29..5a1f5d8 100644 --- a/src/i18n/locales/tr/translation.json +++ b/src/i18n/locales/tr/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Ayarlar...", "checkUpdates": "Güncellemeleri Kontrol Et...", + "copyLastTranscript": "Son transkripti kopyala", "quit": "Çıkış", "cancel": "İptal" }, diff --git a/src/i18n/locales/uk/translation.json b/src/i18n/locales/uk/translation.json index 62e3d16..548ab98 100644 --- a/src/i18n/locales/uk/translation.json +++ b/src/i18n/locales/uk/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "Налаштування...", "checkUpdates": "Перевірити оновлення...", + "copyLastTranscript": "Скопіювати останню транскрипцію", "quit": "Вийти", "cancel": "Скасувати" }, diff --git a/src/i18n/locales/vi/translation.json b/src/i18n/locales/vi/translation.json index e3dbf2e..9d330fb 100644 --- a/src/i18n/locales/vi/translation.json +++ b/src/i18n/locales/vi/translation.json @@ -3,6 +3,7 @@ "tray": { "settings": "Cài đặt...", "checkUpdates": "Kiểm tra cập nhật...", + "copyLastTranscript": "Sao chép bản chép lời mới nhất", "quit": "Thoát", "cancel": "Hủy" }, diff --git a/src/i18n/locales/zh/translation.json b/src/i18n/locales/zh/translation.json index f02b72f..6a1d90b 100644 --- a/src/i18n/locales/zh/translation.json +++ b/src/i18n/locales/zh/translation.json @@ -2,6 +2,7 @@ "tray": { "settings": "设置...", "checkUpdates": "检查更新...", + "copyLastTranscript": "复制最新转录", "quit": "退出", "cancel": "取消" },