feat(tray): add copy last transcript action (#598)

This commit is contained in:
ja 2026-01-17 21:33:50 -05:00 committed by GitHub
parent 031fe4f52e
commit 6522944886
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 187 additions and 0 deletions

View file

@ -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;

View file

@ -379,6 +379,37 @@ impl HistoryManager {
Ok(entries)
}
pub fn get_latest_entry(&self) -> Result<Option<HistoryEntry>> {
let conn = self.get_connection()?;
Self::get_latest_entry_with_conn(&conn)
}
fn get_latest_entry_with_conn(conn: &Connection) -> Result<Option<HistoryEntry>> {
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::<String>::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"));
}
}

View file

@ -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(),
&copy_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(),
&copy_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::<Arc<HistoryManager>>();
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");
}
}

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Nastavení...",
"checkUpdates": "Zkontrolovat aktualizace...",
"copyLastTranscript": "Zkopírovat poslední přepis",
"quit": "Ukončit",
"cancel": "Zrušit"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Einstellungen...",
"checkUpdates": "Nach Updates suchen...",
"copyLastTranscript": "Letzte Transkription kopieren",
"quit": "Beenden",
"cancel": "Abbrechen"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Settings...",
"checkUpdates": "Check for Updates...",
"copyLastTranscript": "Copy Last Transcript",
"quit": "Quit",
"cancel": "Cancel"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Configuración...",
"checkUpdates": "Buscar actualizaciones...",
"copyLastTranscript": "Copiar la última transcripción",
"quit": "Salir",
"cancel": "Cancelar"
},

View file

@ -3,6 +3,7 @@
"tray": {
"settings": "Paramètres...",
"checkUpdates": "Rechercher des mises à jour...",
"copyLastTranscript": "Copier la dernière transcription",
"quit": "Quitter",
"cancel": "Annuler"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Impostazioni...",
"checkUpdates": "Verifica aggiornamenti...",
"copyLastTranscript": "Copia l'ultima trascrizione",
"quit": "Esci",
"cancel": "Annulla"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "設定...",
"checkUpdates": "アップデートを確認...",
"copyLastTranscript": "最新の文字起こしをコピー",
"quit": "終了",
"cancel": "キャンセル"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Ustawienia...",
"checkUpdates": "Sprawdź aktualizacje...",
"copyLastTranscript": "Kopiuj ostatnią transkrypcję",
"quit": "Zamknij",
"cancel": "Anuluj"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Configurações...",
"checkUpdates": "Verificar Atualizações...",
"copyLastTranscript": "Copiar última transcrição",
"quit": "Sair",
"cancel": "Cancelar"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Настройки...",
"checkUpdates": "Проверить обновления...",
"copyLastTranscript": "Скопировать последнюю транскрипцию",
"quit": "Выход",
"cancel": "Отмена"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Ayarlar...",
"checkUpdates": "Güncellemeleri Kontrol Et...",
"copyLastTranscript": "Son transkripti kopyala",
"quit": ıkış",
"cancel": "İptal"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "Налаштування...",
"checkUpdates": "Перевірити оновлення...",
"copyLastTranscript": "Скопіювати останню транскрипцію",
"quit": "Вийти",
"cancel": "Скасувати"
},

View file

@ -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"
},

View file

@ -2,6 +2,7 @@
"tray": {
"settings": "设置...",
"checkUpdates": "检查更新...",
"copyLastTranscript": "复制最新转录",
"quit": "退出",
"cancel": "取消"
},