feat(tray): add copy last transcript action (#598)
This commit is contained in:
parent
031fe4f52e
commit
6522944886
17 changed files with 187 additions and 0 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Nastavení...",
|
||||
"checkUpdates": "Zkontrolovat aktualizace...",
|
||||
"copyLastTranscript": "Zkopírovat poslední přepis",
|
||||
"quit": "Ukončit",
|
||||
"cancel": "Zrušit"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Einstellungen...",
|
||||
"checkUpdates": "Nach Updates suchen...",
|
||||
"copyLastTranscript": "Letzte Transkription kopieren",
|
||||
"quit": "Beenden",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Settings...",
|
||||
"checkUpdates": "Check for Updates...",
|
||||
"copyLastTranscript": "Copy Last Transcript",
|
||||
"quit": "Quit",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Configuración...",
|
||||
"checkUpdates": "Buscar actualizaciones...",
|
||||
"copyLastTranscript": "Copiar la última transcripción",
|
||||
"quit": "Salir",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"tray": {
|
||||
"settings": "Paramètres...",
|
||||
"checkUpdates": "Rechercher des mises à jour...",
|
||||
"copyLastTranscript": "Copier la dernière transcription",
|
||||
"quit": "Quitter",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Impostazioni...",
|
||||
"checkUpdates": "Verifica aggiornamenti...",
|
||||
"copyLastTranscript": "Copia l'ultima trascrizione",
|
||||
"quit": "Esci",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "設定...",
|
||||
"checkUpdates": "アップデートを確認...",
|
||||
"copyLastTranscript": "最新の文字起こしをコピー",
|
||||
"quit": "終了",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Ustawienia...",
|
||||
"checkUpdates": "Sprawdź aktualizacje...",
|
||||
"copyLastTranscript": "Kopiuj ostatnią transkrypcję",
|
||||
"quit": "Zamknij",
|
||||
"cancel": "Anuluj"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Configurações...",
|
||||
"checkUpdates": "Verificar Atualizações...",
|
||||
"copyLastTranscript": "Copiar última transcrição",
|
||||
"quit": "Sair",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Настройки...",
|
||||
"checkUpdates": "Проверить обновления...",
|
||||
"copyLastTranscript": "Скопировать последнюю транскрипцию",
|
||||
"quit": "Выход",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Ayarlar...",
|
||||
"checkUpdates": "Güncellemeleri Kontrol Et...",
|
||||
"copyLastTranscript": "Son transkripti kopyala",
|
||||
"quit": "Çıkış",
|
||||
"cancel": "İptal"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "Налаштування...",
|
||||
"checkUpdates": "Перевірити оновлення...",
|
||||
"copyLastTranscript": "Скопіювати останню транскрипцію",
|
||||
"quit": "Вийти",
|
||||
"cancel": "Скасувати"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"tray": {
|
||||
"settings": "设置...",
|
||||
"checkUpdates": "检查更新...",
|
||||
"copyLastTranscript": "复制最新转录",
|
||||
"quit": "退出",
|
||||
"cancel": "取消"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue